diff --git a/.codecov.yml b/.codecov.yml index 6df3786197..b97039e8b6 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -7,13 +7,13 @@ comment: show_carryforward_flags: false coverage: - range: "60..80" + range: "70..85" precision: 1 round: nearest status: project: default: - target: 60% + target: 75% threshold: 2% patch: default: diff --git a/.github/actions/dependencies/action.yml b/.github/actions/dependencies/action.yml index afce1557d3..eeb8df105c 100644 --- a/.github/actions/dependencies/action.yml +++ b/.github/actions/dependencies/action.yml @@ -6,36 +6,32 @@ inputs: runs: using: composite steps: - - name: unlock Conan - shell: bash - run: conan remove --locks - name: export custom recipes shell: bash run: | - conan config set general.revisions_enabled=1 - conan export external/snappy snappy/1.1.10@ - conan export external/rocksdb rocksdb/9.7.3@ - conan export external/soci soci/4.0.3@ - conan export external/nudb nudb/2.0.8@ + conan export --version 1.1.10 external/snappy + conan export --version 9.7.3 external/rocksdb + conan export --version 4.0.3 external/soci - name: add Ripple Conan remote + if: env.CONAN_URL != '' shell: bash run: | - conan remote list - conan remote remove ripple || true - # Do not quote the URL. An empty string will be accepted (with - # a non-fatal warning), but a missing argument will not. - conan remote add ripple ${{ env.CONAN_URL }} --insert 0 + if conan remote list | grep -q "ripple"; then + conan remote remove ripple + echo "Removed conan remote ripple" + fi + conan remote add --index 0 ripple "${CONAN_URL}" + echo "Added conan remote ripple at ${CONAN_URL}" + - name: try to authenticate to Ripple Conan remote + if: env.CONAN_LOGIN_USERNAME_RIPPLE != '' && env.CONAN_PASSWORD_RIPPLE != '' id: remote shell: bash run: | - # `conan user` implicitly uses the environment variables - # CONAN_LOGIN_USERNAME_ and CONAN_PASSWORD_. - # https://docs.conan.io/1/reference/commands/misc/user.html#using-environment-variables - # https://docs.conan.io/1/reference/env_vars.html#conan-login-username-conan-login-username-remote-name - # https://docs.conan.io/1/reference/env_vars.html#conan-password-conan-password-remote-name - echo outcome=$(conan user --remote ripple --password >&2 \ - && echo success || echo failure) | tee ${GITHUB_OUTPUT} + echo "Authenticating to ripple remote..." + conan remote auth ripple --force + conan remote list-users + - name: list missing binaries id: binaries shell: bash @@ -51,7 +47,7 @@ runs: conan install \ --output-folder . \ --build missing \ - --options tests=True \ - --options xrpld=True \ - --settings build_type=${{ inputs.configuration }} \ + --options:host "&:tests=True" \ + --options:host "&:xrpld=True" \ + --settings:all build_type=${{ inputs.configuration }} \ .. diff --git a/.github/workflows/clang-format.yml b/.github/workflows/clang-format.yml index ac6154ab9f..83752c4780 100644 --- a/.github/workflows/clang-format.yml +++ b/.github/workflows/clang-format.yml @@ -9,24 +9,16 @@ jobs: check: if: ${{ github.event_name == 'push' || github.event.pull_request.draft != true || contains(github.event.pull_request.labels.*.name, 'DraftRunCI') }} runs-on: ubuntu-24.04 - env: - CLANG_VERSION: 18 + container: ghcr.io/xrplf/ci/tools-rippled-clang-format steps: - uses: actions/checkout@v4 - - name: Install clang-format - run: | - codename=$( lsb_release --codename --short ) - sudo tee /etc/apt/sources.list.d/llvm.list >/dev/null <> $GITHUB_PATH + brew install conan - name: install Ninja if: matrix.generator == 'Ninja' run: brew install ninja - name: install python - run: | + run: | if which python > /dev/null 2>&1; then echo "Python executable exists" else @@ -76,14 +89,27 @@ jobs: clang --version - name: configure Conan run : | - conan profile new default --detect || true - conan profile update settings.compiler.cppstd=20 default + echo "${CONAN_GLOBAL_CONF}" >> $(conan config home)/global.conf + conan config install conan/profiles/ -tf $(conan config home)/profiles/ + conan profile show + - name: export custom recipes + shell: bash + run: | + conan export --version 1.1.10 external/snappy + conan export --version 9.7.3 external/rocksdb + conan export --version 4.0.3 external/soci + - name: add Ripple Conan remote + if: env.CONAN_URL != '' + shell: bash + run: | + if conan remote list | grep -q "ripple"; then + conan remote remove ripple + echo "Removed conan remote ripple" + fi + conan remote add --index 0 ripple "${CONAN_URL}" + echo "Added conan remote ripple at ${CONAN_URL}" - name: build dependencies uses: ./.github/actions/dependencies - env: - CONAN_URL: http://18.143.149.228:8081/artifactory/api/conan/conan-non-prod - CONAN_LOGIN_USERNAME_RIPPLE: ${{ secrets.CONAN_USERNAME }} - CONAN_PASSWORD_RIPPLE: ${{ secrets.CONAN_TOKEN }} with: configuration: ${{ matrix.configuration }} - name: build @@ -96,4 +122,7 @@ jobs: run: | n=$(nproc) echo "Using $n test jobs" - ${build_dir}/rippled --unittest --unittest-jobs $n + + cd ${build_dir} + ./rippled --unittest --unittest-jobs $n + ctest -j $n --output-on-failure diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index de59e07761..8218dcc276 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -16,6 +16,21 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +# This part of Conan configuration is specific to this workflow only; we do not want +# to pollute conan/profiles directory with settings which might not work for others +env: + CONAN_URL: http://18.143.149.228:8081/artifactory/api/conan/dev + CONAN_LOGIN_USERNAME_RIPPLE: ${{ secrets.CONAN_USERNAME }} + CONAN_PASSWORD_RIPPLE: ${{ secrets.CONAN_TOKEN }} + CONAN_GLOBAL_CONF: | + core.download:parallel={{ os.cpu_count() }} + core.upload:parallel={{ os.cpu_count() }} + core:default_build_profile=libxrpl + core:default_profile=libxrpl + tools.build:jobs={{ (os.cpu_count() * 4/5) | int }} + tools.build:verbosity=verbose + tools.compilation:verbosity=verbose + # This workflow has multiple job matrixes. # They can be considered phases because most of the matrices ("test", # "coverage", "conan", ) depend on the first ("dependencies"). @@ -54,59 +69,46 @@ jobs: - Release include: - compiler: gcc - profile: - version: 11 - cc: /usr/bin/gcc - cxx: /usr/bin/g++ + compiler_version: 12 + distro: ubuntu + codename: jammy - compiler: clang - profile: - version: 14 - cc: /usr/bin/clang-14 - cxx: /usr/bin/clang++-14 + compiler_version: 16 + distro: debian + codename: bookworm runs-on: [self-hosted, heavy] - container: ghcr.io/xrplf/rippled-build-ubuntu:aaf5e3e + container: ghcr.io/xrplf/ci/${{ matrix.distro }}-${{ matrix.codename }}:${{ matrix.compiler }}-${{ matrix.compiler_version }} env: build_dir: .build steps: - - name: upgrade conan - run: | - pip install --upgrade "conan<2" - name: checkout - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - name: check environment run: | echo ${PATH} | tr ':' '\n' lsb_release -a || true - ${{ matrix.profile.cc }} --version + ${{ matrix.compiler }}-${{ matrix.compiler_version }} --version conan --version cmake --version env | sort - name: configure Conan run: | - conan profile new default --detect - conan profile update settings.compiler.cppstd=20 default - conan profile update settings.compiler=${{ matrix.compiler }} default - conan profile update settings.compiler.version=${{ matrix.profile.version }} default - conan profile update settings.compiler.libcxx=libstdc++11 default - conan profile update env.CC=${{ matrix.profile.cc }} default - conan profile update env.CXX=${{ matrix.profile.cxx }} default - conan profile update conf.tools.build:compiler_executables='{"c": "${{ matrix.profile.cc }}", "cpp": "${{ matrix.profile.cxx }}"}' default + echo "${CONAN_GLOBAL_CONF}" >> $(conan config home)/global.conf + conan config install conan/profiles/ -tf $(conan config home)/profiles/ + conan profile show - name: archive profile # Create this archive before dependencies are added to the local cache. - run: tar -czf conan.tar -C ~/.conan . + run: tar -czf conan.tar.gz -C ${CONAN_HOME} . - name: build dependencies uses: ./.github/actions/dependencies - env: - CONAN_URL: http://18.143.149.228:8081/artifactory/api/conan/conan-non-prod - CONAN_LOGIN_USERNAME_RIPPLE: ${{ secrets.CONAN_USERNAME }} - CONAN_PASSWORD_RIPPLE: ${{ secrets.CONAN_TOKEN }} + with: configuration: ${{ matrix.configuration }} - name: upload archive - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: name: ${{ matrix.platform }}-${{ matrix.compiler }}-${{ matrix.configuration }} - path: conan.tar + path: conan.tar.gz if-no-files-found: error test: @@ -121,26 +123,32 @@ jobs: configuration: - Debug - Release + include: + - compiler: gcc + compiler_version: 12 + distro: ubuntu + codename: jammy + - compiler: clang + compiler_version: 16 + distro: debian + codename: bookworm cmake-args: - - "-Dunity=ON" needs: dependencies runs-on: [self-hosted, heavy] - container: ghcr.io/xrplf/rippled-build-ubuntu:aaf5e3e + container: ghcr.io/xrplf/ci/${{ matrix.distro }}-${{ matrix.codename }}:${{ matrix.compiler }}-${{ matrix.compiler_version }} env: build_dir: .build steps: - - name: upgrade conan - run: | - pip install --upgrade "conan<2" - name: download cache - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 with: name: ${{ matrix.platform }}-${{ matrix.compiler }}-${{ matrix.configuration }} - name: extract cache run: | - mkdir -p ~/.conan - tar -xzf conan.tar -C ~/.conan + mkdir -p ${CONAN_HOME} + tar -xzf conan.tar.gz -C ${CONAN_HOME} - name: check environment run: | env | sort @@ -148,11 +156,9 @@ jobs: conan --version cmake --version - name: checkout - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - name: dependencies uses: ./.github/actions/dependencies - env: - CONAN_URL: http://18.143.149.228:8081/artifactory/api/conan/conan-non-prod with: configuration: ${{ matrix.configuration }} - name: build @@ -161,9 +167,21 @@ jobs: generator: Ninja configuration: ${{ matrix.configuration }} cmake-args: "-Dassert=TRUE -Dwerr=TRUE ${{ matrix.cmake-args }}" + - name: check linking + run: | + cd ${build_dir} + ldd ./rippled + if [ "$(ldd ./rippled | grep -E '(libstdc\+\+|libgcc)' | wc -l)" -eq 0 ]; then + echo 'The binary is statically linked.' + else + echo 'The binary is dynamically linked.' + exit 1 + fi - name: test run: | - ${build_dir}/rippled --unittest --unittest-jobs $(nproc) + cd ${build_dir} + ./rippled --unittest --unittest-jobs $(nproc) + ctest -j $(nproc) --output-on-failure reference-fee-test: strategy: @@ -180,21 +198,18 @@ jobs: - "-DUNIT_TEST_REFERENCE_FEE=1000" needs: dependencies runs-on: [self-hosted, heavy] - container: ghcr.io/xrplf/rippled-build-ubuntu:aaf5e3e + container: ghcr.io/xrplf/ci/ubuntu-jammy:gcc-12 env: build_dir: .build steps: - - name: upgrade conan - run: | - pip install --upgrade "conan<2" - name: download cache - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 with: name: ${{ matrix.platform }}-${{ matrix.compiler }}-${{ matrix.configuration }} - name: extract cache run: | - mkdir -p ~/.conan - tar -xzf conan.tar -C ~/.conan + mkdir -p ${CONAN_HOME} + tar -xzf conan.tar.gz -C ${CONAN_HOME} - name: check environment run: | env | sort @@ -202,11 +217,9 @@ jobs: conan --version cmake --version - name: checkout - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - name: dependencies uses: ./.github/actions/dependencies - env: - CONAN_URL: http://18.143.149.228:8081/artifactory/api/conan/conan-non-prod with: configuration: ${{ matrix.configuration }} - name: build @@ -217,7 +230,9 @@ jobs: cmake-args: "-Dassert=TRUE -Dwerr=TRUE ${{ matrix.cmake-args }}" - name: test run: | - ${build_dir}/rippled --unittest --unittest-jobs $(nproc) + cd ${build_dir} + ./rippled --unittest --unittest-jobs $(nproc) + ctest -j $(nproc) --output-on-failure coverage: strategy: @@ -231,23 +246,18 @@ jobs: - Debug needs: dependencies runs-on: [self-hosted, heavy] - container: ghcr.io/xrplf/rippled-build-ubuntu:aaf5e3e + container: ghcr.io/xrplf/ci/ubuntu-jammy:gcc-12 env: build_dir: .build steps: - - name: upgrade conan - run: | - pip install --upgrade "conan<2" - name: download cache - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 with: name: ${{ matrix.platform }}-${{ matrix.compiler }}-${{ matrix.configuration }} - name: extract cache run: | - mkdir -p ~/.conan - tar -xzf conan.tar -C ~/.conan - - name: install gcovr - run: pip install "gcovr>=7,<9" + mkdir -p ${CONAN_HOME} + tar -xzf conan.tar.gz -C ${CONAN_HOME} - name: check environment run: | echo ${PATH} | tr ':' '\n' @@ -255,13 +265,11 @@ jobs: cmake --version gcovr --version env | sort - ls ~/.conan + ls ${CONAN_HOME} - name: checkout - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - name: dependencies uses: ./.github/actions/dependencies - env: - CONAN_URL: http://18.143.149.228:8081/artifactory/api/conan/conan-non-prod with: configuration: ${{ matrix.configuration }} - name: build @@ -283,7 +291,7 @@ jobs: run: | mv "${build_dir}/coverage.xml" ./ - name: archive coverage report - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: name: coverage.xml path: coverage.xml @@ -305,22 +313,23 @@ jobs: conan: needs: dependencies runs-on: [self-hosted, heavy] - container: ghcr.io/xrplf/rippled-build-ubuntu:aaf5e3e + container: + image: ghcr.io/xrplf/ci/ubuntu-jammy:gcc-12 env: build_dir: .build + platform: linux + compiler: gcc + compiler_version: 12 configuration: Release steps: - - name: upgrade conan - run: | - pip install --upgrade "conan<2" - name: download cache - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 with: - name: linux-gcc-${{ env.configuration }} + name: ${{ env.platform }}-${{ env.compiler }}-${{ env.configuration }} - name: extract cache run: | - mkdir -p ~/.conan - tar -xzf conan.tar -C ~/.conan + mkdir -p ${CONAN_HOME} + tar -xzf conan.tar.gz -C ${CONAN_HOME} - name: check environment run: | env | sort @@ -328,27 +337,22 @@ jobs: conan --version cmake --version - name: checkout - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - name: dependencies uses: ./.github/actions/dependencies - env: - CONAN_URL: http://18.143.149.228:8081/artifactory/api/conan/conan-non-prod with: configuration: ${{ env.configuration }} - name: export run: | - version=$(conan inspect --raw version .) - reference="xrpl/${version}@local/test" - conan remove -f ${reference} || true - conan export . local/test - echo "reference=${reference}" >> "${GITHUB_ENV}" + conan export . --version head - name: build run: | cd tests/conan - mkdir ${build_dir} - cd ${build_dir} - conan install .. --output-folder . \ - --require-override ${reference} --build missing + mkdir ${build_dir} && cd ${build_dir} + conan install .. \ + --settings:all build_type=${configuration} \ + --output-folder . \ + --build missing cmake .. \ -DCMAKE_TOOLCHAIN_FILE:FILEPATH=./build/${configuration}/generators/conan_toolchain.cmake \ -DCMAKE_BUILD_TYPE=${configuration} @@ -363,60 +367,31 @@ jobs: if: ${{ github.event_name == 'push' || github.event.pull_request.draft != true || contains(github.event.pull_request.labels.*.name, 'DraftRunCI') }} env: CLANG_RELEASE: 16 - strategy: - fail-fast: false runs-on: [self-hosted, heavy] - container: debian:bookworm - steps: - - name: install prerequisites - env: - DEBIAN_FRONTEND: noninteractive - run: | - apt-get update - apt-get install --yes --no-install-recommends \ - clang-${CLANG_RELEASE} clang++-${CLANG_RELEASE} \ - python3-pip python-is-python3 make cmake git wget - apt-get clean - update-alternatives --install \ - /usr/bin/clang clang /usr/bin/clang-${CLANG_RELEASE} 100 \ - --slave /usr/bin/clang++ clang++ /usr/bin/clang++-${CLANG_RELEASE} - update-alternatives --auto clang - pip install --no-cache --break-system-packages "conan<2" + container: ghcr.io/xrplf/ci/debian-bookworm:clang-16 + steps: - name: checkout - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - name: prepare environment run: | mkdir ${GITHUB_WORKSPACE}/.build echo "SOURCE_DIR=$GITHUB_WORKSPACE" >> $GITHUB_ENV echo "BUILD_DIR=$GITHUB_WORKSPACE/.build" >> $GITHUB_ENV - echo "CC=/usr/bin/clang" >> $GITHUB_ENV - echo "CXX=/usr/bin/clang++" >> $GITHUB_ENV - name: configure Conan run: | - conan profile new --detect default - conan profile update settings.compiler=clang default - conan profile update settings.compiler.version=${CLANG_RELEASE} default - conan profile update settings.compiler.libcxx=libstdc++11 default - conan profile update settings.compiler.cppstd=20 default - conan profile update options.rocksdb=False default - conan profile update \ - 'conf.tools.build:compiler_executables={"c": "/usr/bin/clang", "cpp": "/usr/bin/clang++"}' default - conan profile update 'env.CXXFLAGS="-DBOOST_ASIO_DISABLE_CONCEPTS"' default - conan profile update 'conf.tools.build:cxxflags+=["-DBOOST_ASIO_DISABLE_CONCEPTS"]' default - conan export external/snappy snappy/1.1.10@ - conan export external/soci soci/4.0.3@ - + echo "${CONAN_GLOBAL_CONF}" >> $(conan config home)/global.conf + conan config install conan/profiles/ -tf $(conan config home)/profiles/ + conan profile show - name: build dependencies run: | cd ${BUILD_DIR} conan install ${SOURCE_DIR} \ --output-folder ${BUILD_DIR} \ - --install-folder ${BUILD_DIR} \ --build missing \ - --settings build_type=Debug + --settings:all build_type=Debug - name: build with instrumentation run: | @@ -441,3 +416,4 @@ jobs: run: | cd ${BUILD_DIR} ./rippled -u --unittest-jobs $(( $(nproc)/4 )) + ctest -j $(nproc) --output-on-failure diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 1d90c2ef58..254850f26a 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -18,6 +18,20 @@ on: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +# This part of Conan configuration is specific to this workflow only; we do not want +# to pollute conan/profiles directory with settings which might not work for others +env: + CONAN_URL: http://18.143.149.228:8081/artifactory/api/conan/dev + CONAN_LOGIN_USERNAME_RIPPLE: ${{ secrets.CONAN_USERNAME }} + CONAN_PASSWORD_RIPPLE: ${{ secrets.CONAN_TOKEN }} + CONAN_GLOBAL_CONF: | + core.download:parallel={{os.cpu_count()}} + core.upload:parallel={{os.cpu_count()}} + core:default_build_profile=libxrpl + core:default_profile=libxrpl + tools.build:jobs=24 + tools.build:verbosity=verbose + tools.compilation:verbosity=verbose jobs: @@ -42,11 +56,11 @@ jobs: build_dir: .build steps: - name: checkout - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - name: choose Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 with: - python-version: 3.9 + python-version: 3.13 - name: learn Python cache directory id: pip-cache shell: bash @@ -54,12 +68,12 @@ jobs: python -m pip install --upgrade pip echo "dir=$(pip cache dir)" | tee ${GITHUB_OUTPUT} - name: restore Python cache directory - uses: actions/cache@v4 + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 with: path: ${{ steps.pip-cache.outputs.dir }} key: ${{ runner.os }}-${{ hashFiles('.github/workflows/windows.yml') }} - name: install Conan - run: pip install wheel 'conan<2' + run: pip install wheel conan - name: check environment run: | dir env: @@ -70,17 +84,27 @@ jobs: - name: configure Conan shell: bash run: | - conan profile new default --detect - conan profile update settings.compiler.cppstd=20 default - conan profile update \ - settings.compiler.runtime=MT${{ matrix.configuration.runtime }} \ - default + echo "${CONAN_GLOBAL_CONF}" >> $(conan config home)/global.conf + conan config install conan/profiles/ -tf $(conan config home)/profiles/ + conan profile show + - name: export custom recipes + shell: bash + run: | + conan export --version 1.1.10 external/snappy + conan export --version 9.7.3 external/rocksdb + conan export --version 4.0.3 external/soci + - name: add Ripple Conan remote + if: env.CONAN_URL != '' + shell: bash + run: | + if conan remote list | grep -q "ripple"; then + conan remote remove ripple + echo "Removed conan remote ripple" + fi + conan remote add --index 0 ripple "${CONAN_URL}" + echo "Added conan remote ripple at ${CONAN_URL}" - name: build dependencies uses: ./.github/actions/dependencies - env: - CONAN_URL: http://18.143.149.228:8081/artifactory/api/conan/conan-non-prod - CONAN_LOGIN_USERNAME_RIPPLE: ${{ secrets.CONAN_USERNAME }} - CONAN_PASSWORD_RIPPLE: ${{ secrets.CONAN_TOKEN }} with: configuration: ${{ matrix.configuration.type }} - name: build @@ -95,5 +119,6 @@ jobs: shell: bash if: ${{ matrix.configuration.tests }} run: | - ${build_dir}/${{ matrix.configuration.type }}/rippled --unittest \ - --unittest-jobs $(nproc) + cd ${build_dir}/${{ matrix.configuration.type }} + ./rippled --unittest --unittest-jobs $(nproc) + ctest -j $(nproc) --output-on-failure diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9f69d41379..abfbd887c7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ # .pre-commit-config.yaml repos: - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v18.1.3 + rev: v18.1.8 hooks: - id: clang-format diff --git a/BUILD.md b/BUILD.md index fd985dce81..fba238d2bc 100644 --- a/BUILD.md +++ b/BUILD.md @@ -167,8 +167,6 @@ It does not explicitly link the C++ standard library, which allows you to statically link it with GCC, if you want. ``` - # Conan 1.x - conan export external/snappy snappy/1.1.10@ # Conan 2.x conan export --version 1.1.10 external/snappy ``` @@ -177,8 +175,6 @@ Export our [Conan recipe for RocksDB](./external/rocksdb). It does not override paths to dependencies when building with Visual Studio. ``` - # Conan 1.x - conan export external/rocksdb rocksdb/9.7.3@ # Conan 2.x conan export --version 9.7.3 external/rocksdb ``` @@ -187,23 +183,10 @@ Export our [Conan recipe for SOCI](./external/soci). It patches their CMake to correctly import its dependencies. ``` - # Conan 1.x - conan export external/soci soci/4.0.3@ # Conan 2.x conan export --version 4.0.3 external/soci ``` -Export our [Conan recipe for NuDB](./external/nudb). -It fixes some source files to add missing `#include`s. - - - ``` - # Conan 1.x - conan export external/nudb nudb/2.0.8@ - # Conan 2.x - conan export --version 2.0.8 external/nudb - ``` - ### Build and Test 1. Create a build directory and move into it. @@ -288,7 +271,7 @@ It fixes some source files to add missing `#include`s. Single-config generators: ``` - cmake --build . + cmake --build . -j $(nproc) ``` Multi-config generators: diff --git a/Builds/levelization/results/ordering.txt b/Builds/levelization/results/ordering.txt index eca7fc6dc2..ce22d8edb0 100644 --- a/Builds/levelization/results/ordering.txt +++ b/Builds/levelization/results/ordering.txt @@ -132,6 +132,7 @@ test.shamap > xrpl.protocol test.toplevel > test.csf test.toplevel > xrpl.json test.unit_test > xrpl.basics +tests.libxrpl > xrpl.basics xrpl.json > xrpl.basics xrpl.protocol > xrpl.basics xrpl.protocol > xrpl.json diff --git a/CMakeLists.txt b/CMakeLists.txt index a9f063db57..c71fb68599 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -90,6 +90,11 @@ set_target_properties(OpenSSL::SSL PROPERTIES INTERFACE_COMPILE_DEFINITIONS OPENSSL_NO_SSL2 ) set(SECP256K1_INSTALL TRUE) +set(SECP256K1_BUILD_BENCHMARK FALSE) +set(SECP256K1_BUILD_TESTS FALSE) +set(SECP256K1_BUILD_EXHAUSTIVE_TESTS FALSE) +set(SECP256K1_BUILD_CTIME_TESTS FALSE) +set(SECP256K1_BUILD_EXAMPLES FALSE) add_subdirectory(external/secp256k1) add_library(secp256k1::secp256k1 ALIAS secp256k1) add_subdirectory(external/ed25519-donna) @@ -144,3 +149,8 @@ set(PROJECT_EXPORT_SET RippleExports) include(RippledCore) include(RippledInstall) include(RippledValidatorKeys) + +if(tests) + include(CTest) + add_subdirectory(src/tests/libxrpl) +endif() diff --git a/README.md b/README.md index cc002a2dd8..0315c37428 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![codecov](https://codecov.io/gh/XRPLF/rippled/graph/badge.svg?token=WyFr5ajq3O)](https://codecov.io/gh/XRPLF/rippled) + # The XRP Ledger The [XRP Ledger](https://xrpl.org/) is a decentralized cryptographic ledger powered by a network of peer-to-peer nodes. The XRP Ledger uses a novel Byzantine Fault Tolerant consensus algorithm to settle and record transactions in a secure distributed database without a central operator. diff --git a/RELEASENOTES.md b/RELEASENOTES.md deleted file mode 100644 index 6bc7beccc7..0000000000 --- a/RELEASENOTES.md +++ /dev/null @@ -1,4817 +0,0 @@ -# Release Notes - -![XRP](docs/images/xrp-text-mark-black-small@2x.png) - -This document contains the release notes for `rippled`, the reference server implementation of the XRP Ledger protocol. To learn more about how to build, run or update a `rippled` server, visit https://xrpl.org/install-rippled.html - -Have new ideas? Need help with setting up your node? [Please open an issue here](https://github.com/xrplf/rippled/issues/new/choose). -## Full Changelog - -### Amendments - -The following amendments are open for voting with this release: - -- **DynamicNFT (XLS-46)** - Adds the ability to mint mutable `NFToken` objects whose URI can be changed. ([#5048](https://github.com/XRPLF/rippled/pull/5048)) -- **PermissionedDomains (XLS-80)** - Adds Permissioned Domains, which act as part of broader systems on the XRP Ledger to restrict access to satisfy compliance rules. ([#5161](https://github.com/XRPLF/rippled/pull/5161)) -- **DeepFreeze (XLS-77)** - Adds the ability to deep freeze trust lines, enabling token issuers to block the transfer of assets for holders who have been deep frozen. ([#5187](https://github.com/XRPLF/rippled/pull/5187)) -- **fixFrozenLPTokenTransfer** - Prohibits the transfer of LP tokens when the associated liquidity pool contains at least one frozen asset. ([#5227](https://github.com/XRPLF/rippled/pull/5227)) -- **fixInvalidTxFlags** - Adds transaction flag checking for `CredentialCreate`, `CredentialAccept`, and `CredentialDelete` transactions. ([#5250](https://github.com/XRPLF/rippled/pull/5250)) - - -### New Features - -- Added a new `simulate` API method to execute dry runs of transactions and see the simulated metadata. ([#5069](https://github.com/XRPLF/rippled/pull/5069), [#5265](https://github.com/XRPLF/rippled/pull/5265)) -- Added the ability to specify MPTs when defining assets in transactions. ([#5200](https://github.com/XRPLF/rippled/pull/5200)) -- Added a `state` alias for `ripple_state` in the `ledger_entry` API method. Also refactored `LedgerEntry.cpp` to make it easier to read. ([#5199](https://github.com/XRPLF/rippled/pull/5199)) -- Improved UNL security by enabling validators to set a minimum number of UNL publishers to agree on validators. ([#5112](https://github.com/XRPLF/rippled/pull/5112)) -- Updated the XRPL Foundation UNL keys. ([#5289](https://github.com/XRPLF/rippled/pull/5289)) -- Added a new XRPL Foundation subdomain to enable a staged migration without modifying the key for the current UNL list. ([#5326](https://github.com/XRPLF/rippled/pull/5326)) -- Added support to filter ledger entry types by their canonical names in the `ledger`, `ledger_data`, and `account_objects` API methods. ([#5271](https://github.com/XRPLF/rippled/pull/5271)) -- Added detailed logging for each validation and proposal received from the network. ([#5291](https://github.com/XRPLF/rippled/pull/5291)) -- Improved git commit hash lookups when checking the version of a `rippled` debug build. Also added git commit hash info when using the `server_info` API method on an admin connection. ([#5225](https://github.com/XRPLF/rippled/pull/5225)) - - -### Bug fixes - -- Fixed an issue with overlapping data types in the `Expected` class. ([#5218](https://github.com/XRPLF/rippled/pull/5218)) -- Fixed an issue that prevented `rippled` from building on Windows with VS2022. ([#5197](https://github.com/XRPLF/rippled/pull/5197)) -- Fixed `server_definitions` prefixes. ([#5231](https://github.com/XRPLF/rippled/pull/5231)) -- Added missing dependency installations for generic MasOS runners. ([#5233](https://github.com/XRPLF/rippled/pull/5233)) -- Updated deprecated Github actions. ([#5241](https://github.com/XRPLF/rippled/pull/5241)) -- Fixed a failing assert scenario when submitting the `connect` admin RPC. ([#5235](https://github.com/XRPLF/rippled/pull/5235)) -- Fixed the levelization script to ignore single-line comments during dependency analysis. ([#5194](https://github.com/XRPLF/rippled/pull/5194)) -- Fixed the assert name used in `PermissionedDomainDelete`. ([#5245](https://github.com/XRPLF/rippled/pull/5245)) -- Fixed macOS unit tests. ([#5196](https://github.com/XRPLF/rippled/pull/5196)) -- Fixed an issue with validators not accurately reflecting amendment votes. Also added debug logging of amendment votes. ([#5173](https://github.com/XRPLF/rippled/pull/5173), [#5312](https://github.com/XRPLF/rippled/pull/5312)) -- Fixed a potential issue with double-charging fees. ([#5269](https://github.com/XRPLF/rippled/pull/5269)) -- Removed the `new parent hash` assert and replaced it with a log message. ([#5313](https://github.com/XRPLF/rippled/pull/5313)) -- Fixed an issue that prevented previously-failed inbound ledgers to not be acquired if a new trusted proposal arrived. ([#5318](https://github.com/XRPLF/rippled/pull/5318)) - - -### Other Improvements - -- Added unit tests for `AccountID` handling. ([#5174](https://github.com/XRPLF/rippled/pull/5174)) -- Added enforced levelization in `libxrpl` with CMake. ([#5199](https://github.com/XRPLF/rippled/pull/5111)) -- Updated `libxrpl` and all submodules to use the same compiler options. ([#5228](https://github.com/XRPLF/rippled/pull/5228)) -- Added Antithesis instrumentation. ([#5042](https://github.com/XRPLF/rippled/pull/5042), [#5213](https://github.com/XRPLF/rippled/pull/5213)) -- Added `rpcName` to the `LEDGER_ENTRY` macro to help prevent future bugs. ([#5202](https://github.com/XRPLF/rippled/pull/5202)) -- Updated the contribution guidelines to introduce a new workflow that avoids code freezes. Also added scripts that can be used by maintainers in branch management, and a CI job to check that code is consistent across the three main branches: `master`, `release`, and `develop`. ([#5215](https://github.com/XRPLF/rippled/pull/5215)) -- Added unit tests to check for caching issues fixed in `rippled 2.3.0`. ([#5242](https://github.com/XRPLF/rippled/pull/5242)) -- Cleaned up the API changelog. ([#5207](https://github.com/XRPLF/rippled/pull/5207)) -- Improved logs readability. ([#5251](https://github.com/XRPLF/rippled/pull/5251)) -- Updated Visual Studio CI to VS 2022, and added VS Debug builds. ([#5240](https://github.com/XRPLF/rippled/pull/5240)) -- Updated the `secp256k1` library to version 0.6.0. ([#5254](https://github.com/XRPLF/rippled/pull/5254)) -- Changed the `[port_peer]` parameter in `rippled` example config back to `51235`; also added the recommendation to use the default port of `2459` for new deployments. ([#5290](https://github.com/XRPLF/rippled/pull/5290), [#5299](https://github.com/XRPLF/rippled/pull/5299)) -- Improved CI management. ([#5268](https://github.com/XRPLF/rippled/pull/5268)) -- Updated the git commit message rules for contributors. ([#5283](https://github.com/XRPLF/rippled/pull/5283)) -- Fixed unnecessary `setCurrentThreadName` calls. ([#5280](https://github.com/XRPLF/rippled/pull/5280)) -- Added a check to prevent permissioned domains from being created in the event the Permissioned Domains amendement is enabled before the Credentials amendement. ([#5275](https://github.com/XRPLF/rippled/pull/5275)) -- Updated Conan dependencies. ([#5256](https://github.com/XRPLF/rippled/pull/5256)) -- Fixed minor typos in code comments. ([#5279](https://github.com/XRPLF/rippled/pull/5279)) -- Fixed incorrect build instructions. ([#5274](https://github.com/XRPLF/rippled/pull/5274)) -- Refactored `rotateWithLock()` to not hold a lock during callbacks. ([#5276](https://github.com/XRPLF/rippled/pull/5276)) -- Cleaned up debug logging by combining multiple data points into a single message. ([#5302](https://github.com/XRPLF/rippled/pull/5302)) -- Updated build flags to fix performance regressions. ([#5325](https://github.com/XRPLF/rippled/pull/5325)) - - -## Credits - -The following people contributed directly to this release: - -- Aanchal Malhotra -- Bart Thomee <11445373+bthomee@users.noreply.github.com> -- Bronek Kozicki -- code0xff -- Darius Tumas -- David Fuelling -- Donovan Hide -- Ed Hennis -- Elliot Lee -- Javier Romero -- Kenny Lei -- Mark Travis <7728157+mtrippled@users.noreply.github.com> -- Mayukha Vadari -- Michael Legleux -- Oleksandr <115580134+oleks-rip@users.noreply.github.com> -- Qi Zhao -- Ramkumar Srirengaram Gunasegharan -- Shae Wang -- Shawn Xie -- Sophia Xie -- Vijay Khanna Raviraj -- Vladislav Vysokikh -- Xun Zhao - -## Bug Bounties and Responsible Disclosures - -We welcome reviews of the `rippled` code and urge researchers to responsibly disclose any issues they may find. - -To report a bug, please send a detailed report to: - -# Version 2.3.1 - -Version 2.3.1 of `rippled`, the reference server implementation of the XRP Ledger protocol, is now available. -This is a hotfix release that includes the following updates: -- Fix an erroneous high fee penalty that peers could incur for sending older transactions. -- Update to the fees charged for imposing a load on the server. -- Prevent the relaying of internal pseudo-transactions. - - Before: Pseudo-transactions received from a peer will fail the signature check, even if they were requested (using TMGetObjectByHash) because they have no signature. This causes the peer to be charged for an invalid signature. - - After: Pseudo-transactions, are put into the global cache (TransactionMaster) only. If the transaction is not part of a TMTransactions batch, the peer is charged an unwanted data fee. These fees will not be a problem in the normal course of operations but should dissuade peers from behaving badly by sending a bunch of junk. -- Improved logging now specifies the reason for the fee charged to the peer. - -[Sign Up for Future Release Announcements](https://groups.google.com/g/ripple-server) - - - -## Action Required - -If you run an XRP Ledger validator, upgrade to version 2.3.1 as soon as possible to ensure stable and uninterrupted network behavior. - -## Changelog - -### Amendments and New Features - -- None - -### Bug Fixes and Performance Improvements - -- Change the charged fee for sending older transactions from feeInvalidSignature to feeUnwantedData. [#5243](https://github.com/XRPLF/rippled/pull/5243) - -### Docs and Build System - -- None - -### GitHub - -The public source code repository for `rippled` is hosted on GitHub at . - -We welcome all contributions and invite everyone to join the community of XRP Ledger developers to help build the Internet of Value. - - -## Credits - -The following people contributed directly to this release: - -Ed Hennis -JoelKatz -Sophia Xie <106177003+sophiax851@users.noreply.github.com> -Valentin Balaschenko <13349202+vlntb@users.noreply.github.com> - - -Bug Bounties and Responsible Disclosures: - -We welcome reviews of the `rippled` code and urge researchers to responsibly disclose any issues they may find. - -To report a bug, please send a detailed report to: - -# Version 2.3.0 - -Version 2.3.0 of `rippled`, the reference server implementation of the XRP Ledger protocol, is now available. This release includes 8 new amendments, including Multi-Purpose Tokens, Credentials, Clawback support for AMMs, and the ability to make offers as part of minting NFTs. Additionally, this release includes important fixes for stability, so server operators are encouraged to upgrade as soon as possible. - - -## Action Required - -If you run an XRP Ledger server, upgrade to version 2.3.0 as soon as possible to ensure service continuity. - -Additionally, new amendments are now open for voting according to the XRP Ledger's [amendment process](https://xrpl.org/amendments.html), which enables protocol changes following two weeks of >80% support from trusted validators. The exact time that protocol changes take effect depends on the voting decisions of the decentralized network. - -## Full Changelog - -### Amendments - -The following amendments are open for voting with this release: - -- **XLS-70 Credentials** - Users can issue Credentials on the ledger and use Credentials to pre-approve incoming payments when using Deposit Authorization instead of individually approving payers. ([#5103](https://github.com/XRPLF/rippled/pull/5103)) - - related fix: #5189 (https://github.com/XRPLF/rippled/pull/5189) -- **XLS-33 Multi-Purpose Tokens** - A new type of fungible token optimized for institutional DeFi including stablecoins. ([#5143](https://github.com/XRPLF/rippled/pull/5143)) -- **XLS-37 AMM Clawback** - Allows clawback-enabled tokens to be used in AMMs, with appropriate guardrails. ([#5142](https://github.com/XRPLF/rippled/pull/5142)) -- **XLS-52 NFTokenMintOffer** - Allows creating an NFT sell offer as part of minting a new NFT. ([#4845](https://github.com/XRPLF/rippled/pull/4845)) -- **fixAMMv1_2** - Fixes two bugs in Automated Market Maker (AMM) transaction processing. ([#5176](https://github.com/XRPLF/rippled/pull/5176)) -- **fixNFTokenPageLinks** - Fixes a bug that can cause NFT directories to have missing links, and introduces a transaction to repair corrupted ledger state. ([#4945](https://github.com/XRPLF/rippled/pull/4945)) -- **fixEnforceNFTokenTrustline** - Fixes two bugs in the interaction between NFT offers and trust lines. ([#4946](https://github.com/XRPLF/rippled/pull/4946)) -- **fixInnerObjTemplate2** - Standardizes the way inner objects are enforced across all transaction and ledger data. ([#5047](https://github.com/XRPLF/rippled/pull/5047)) - -The following amendment is partially implemented but not open for voting: - -- **InvariantsV1_1** - Adds new invariants to ensure transactions process as intended, starting with an invariant to ensure that ledger entries owned by an account are deleted when the account is deleted. ([#4663](https://github.com/XRPLF/rippled/pull/4663)) - -### New Features - -- Allow configuration of SQLite database page size. ([#5135](https://github.com/XRPLF/rippled/pull/5135), [#5140](https://github.com/XRPLF/rippled/pull/5140)) -- In the `libxrpl` C++ library, provide a list of known amendments. ([#5026](https://github.com/XRPLF/rippled/pull/5026)) - -### Deprecations - -- History Shards are removed. ([#5066](https://github.com/XRPLF/rippled/pull/5066)) -- Reporting mode is removed. ([#5092](https://github.com/XRPLF/rippled/pull/5092)) - -For users wanting to store more ledger history, it is recommended to run a Clio server instead. - -### Bug fixes - -- Fix a crash in debug builds when amm_info request contains an invalid AMM account ID. ([#5188](https://github.com/XRPLF/rippled/pull/5188)) -- Fix a crash caused by a race condition in peer-to-peer code. ([#5071](https://github.com/XRPLF/rippled/pull/5071)) -- Fix a crash in certain situations -- Fix several bugs in the book_changes API method. ([#5096](https://github.com/XRPLF/rippled/pull/5096)) -- Fix bug triggered by providing an invalid marker to the account_nfts API method. ([#5045](https://github.com/XRPLF/rippled/pull/5045)) -- Accept lower-case hexadecimal in compact transaction identifier (CTID) parameters in API methods. ([#5049](https://github.com/XRPLF/rippled/pull/5049)) -- Disallow filtering by types that an account can't own in the account_objects API method. ([#5056](https://github.com/XRPLF/rippled/pull/5056)) -- Fix error code returned by the feature API method when providing an invalid parameter. ([#5063](https://github.com/XRPLF/rippled/pull/5063)) -- (API v3) Fix error code returned by amm_info when providing invalid parameters. ([#4924](https://github.com/XRPLF/rippled/pull/4924)) - -### Other Improvements - -- Adds a new default hub, hubs.xrpkuwait.com, to the config file and bootstrapping code. ([#5169](https://github.com/XRPLF/rippled/pull/5169)) -- Improve error message when commandline interface fails with `rpcInternal` because there was no response from the server. ([#4959](https://github.com/XRPLF/rippled/pull/4959)) -- Add tools for debugging specific transactions via replay. ([#5027](https://github.com/XRPLF/rippled/pull/5027), [#5087](https://github.com/XRPLF/rippled/pull/5087)) -- Major reorganization of source code files. ([#4997](https://github.com/XRPLF/rippled/pull/4997)) -- Add new unit tests. ([#4886](https://github.com/XRPLF/rippled/pull/4886)) -- Various improvements to build tools and contributor documentation. ([#5001](https://github.com/XRPLF/rippled/pull/5001), [#5028](https://github.com/XRPLF/rippled/pull/5028), [#5052](https://github.com/XRPLF/rippled/pull/5052), [#5091](https://github.com/XRPLF/rippled/pull/5091), [#5084](https://github.com/XRPLF/rippled/pull/5084), [#5120](https://github.com/XRPLF/rippled/pull/5120), [#5010](https://github.com/XRPLF/rippled/pull/5010). [#5055](https://github.com/XRPLF/rippled/pull/5055), [#5067](https://github.com/XRPLF/rippled/pull/5067), [#5061](https://github.com/XRPLF/rippled/pull/5061), [#5072](https://github.com/XRPLF/rippled/pull/5072), [#5044](https://github.com/XRPLF/rippled/pull/5044) ) -- Various code cleanup and refactoring. ([#4509](https://github.com/XRPLF/rippled/pull/4509), [#4521](https://github.com/XRPLF/rippled/pull/4521), [#4856](https://github.com/XRPLF/rippled/pull/4856), [#5190](https://github.com/XRPLF/rippled/pull/5190), [#5081](https://github.com/XRPLF/rippled/pull/5081), [#5053](https://github.com/XRPLF/rippled/pull/5053), [#5058](https://github.com/XRPLF/rippled/pull/5058), [#5122](https://github.com/XRPLF/rippled/pull/5122), [#5059](https://github.com/XRPLF/rippled/pull/5059), [#5041](https://github.com/XRPLF/rippled/pull/5041)) - - -Bug Bounties and Responsible Disclosures: - -We welcome reviews of the `rippled` code and urge researchers to responsibly disclose any issues they may find. - -To report a bug, please send a detailed report to: - - -# Version 2.2.3 - -Version 2.2.3 of `rippled`, the reference server implementation of the XRP Ledger protocol, is now available. This release fixes a problem that can cause full-history servers to run out of space in their SQLite databases, depending on configuration. There are no new amendments in this release. - -[Sign Up for Future Release Announcements](https://groups.google.com/g/ripple-server) - - - -## Background - -The `rippled` server uses a SQLite database for tracking transactions, in addition to the main data store (usually NuDB) for ledger data. In servers keeping a large amount of history, this database can run out of space based on the configured number and size of database pages, even if the machine has disk space available. Based on the size of full history on Mainnet, servers with the default SQLite page size of 4096 may now run out of space if they store full history. In this case, your server may shut down with an error such as the following: - -```text -Free SQLite space for transaction db is less than 512MB. To fix this, rippled - must be executed with the vacuum parameter before restarting. - Note that this activity can take multiple days, depending on database size. -``` - -The exact timing of when a server runs out of space can vary based on a few factors. Server operators who encountered a similar problem in 2018 and followed steps to [increase the SQLite transaction database page size issue](../../../docs/infrastructure/troubleshooting/fix-sqlite-tx-db-page-size-issue) may not encounter this problem at all. The `--vacuum` commandline option to `rippled` from that time may work to free up space in the database, but requires extended downtime. - -Version 2.2.3 of `rippled` reconfigures the maximum number of SQLite pages so that the issue does not occur. - -Clio servers providing full history are not affected by this issue. - - -## Action Required - -If you run an [XRP Ledger full history server](https://xrpl.org/docs/infrastructure/configuration/data-retention/configure-full-history), upgrading to version 2.2.3 may prevent the server from crashing when `transaction.db` exceeds approximately 8.7 terabytes. - -Additionally, five amendments introduced in version 2.2.0 are open for voting according to the XRP Ledger's [amendment process](https://xrpl.org/amendments.html), which enables protocol changes following two weeks of >80% support from trusted validators. If you operate an XRP Ledger server older than version 2.2.0, upgrade by Sep 23, 2024 to ensure service continuity. The exact time that protocol changes take effect depends on the voting decisions of the decentralized network. - -## Changelog - -### Bug Fixes - -- Update SQLite3 max_page_count to match current defaults ([#5114](https://github.com/XRPLF/rippled/pull/5114)) - -### GitHub - -The public source code repository for `rippled` is hosted on GitHub at . - -We welcome all contributions and invite everyone to join the community of XRP Ledger developers to help build the Internet of Value. - - -## Credits - -The following people contributed directly to this release: - -J. Scott Branson - - -Bug Bounties and Responsible Disclosures: - -We welcome reviews of the `rippled` code and urge researchers to responsibly disclose any issues they may find. - -To report a bug, please send a detailed report to: - - -# Version 2.2.2 - -Version 2.2.2 of `rippled`, the reference server implementation of the XRP Ledger protocol, is now available. This release fixes an ongoing issue with Mainnet where validators can stall during consensus processing due to lock contention, preventing ledgers from being validated for up to two minutes. There are no new amendments in this release. - -[Sign Up for Future Release Announcements](https://groups.google.com/g/ripple-server) - - - -## Action Required - -If you run an XRP Ledger validator, upgrade to version 2.2.2 as soon as possible to ensure stable and uninterrupted network behavior. - -Additionally, five amendments introduced in version 2.2.0 are open for voting according to the XRP Ledger's [amendment process](https://xrpl.org/amendments.html), which enables protocol changes following two weeks of >80% support from trusted validators. If you operate an XRP Ledger server older than version 2.2.0, upgrade by September 17, 2024 to ensure service continuity. The exact time that protocol changes take effect depends on the voting decisions of the decentralized network. Version 2.2.2 is recommended because of known bugs affecting stability of versions 2.2.0 and 2.2.1. - -If you operate a Clio server, Clio needs to be updated to 2.1.2 before updating to rippled 2.2.0. Clio will be blocked if it is not updated. - -## Changelog - -### Amendments and New Features - -- None - -### Bug Fixes and Performance Improvements - -- Allow only 1 job queue slot for acquiring inbound ledger [#5115](https://github.com/XRPLF/rippled/pull/5115) ([7741483](https://github.com/XRPLF/rippled/commit/774148389467781aca7c01bac90af2fba870570c)) - -- Allow only 1 job queue slot for each validation ledger check [#5115](https://github.com/XRPLF/rippled/pull/5115) ([fbbea9e](https://github.com/XRPLF/rippled/commit/fbbea9e6e25795a8a6bd1bf64b780771933a9579)) - -### Other improvements - - - Track latencies of certain code blocks, and log if they take too long [#5115](https://github.com/XRPLF/rippled/pull/5115) ([00ed7c9](https://github.com/XRPLF/rippled/commit/00ed7c942436f02644a13169002b5123f4e2a116)) - -### Docs and Build System - -- None - -### GitHub - -The public source code repository for `rippled` is hosted on GitHub at . - -We welcome all contributions and invite everyone to join the community of XRP Ledger developers to help build the Internet of Value. - - -## Credits - -The following people contributed directly to this release: - -Mark Travis -Valentin Balaschenko <13349202+vlntb@users.noreply.github.com> - -Bug Bounties and Responsible Disclosures: - -We welcome reviews of the `rippled` code and urge researchers to responsibly disclose any issues they may find. - -To report a bug, please send a detailed report to: - -# Version 2.2.1 - -Version 2.2.1 of `rippled`, the reference server implementation of the XRP Ledger protocol, is now available. This release fixes a critical bug introduced in 2.2.0 handling some types of RPC requests. - -[Sign Up for Future Release Announcements](https://groups.google.com/g/ripple-server) - - - -## Action Required - -If you run an XRP Ledger validator, upgrade to version 2.2.1 as soon as possible to ensure stable and uninterrupted network behavior. - -Additionally, five amendments introduced in version 2.2.0 are open for voting according to the XRP Ledger's [amendment process](https://xrpl.org/amendments.html), which enables protocol changes following two weeks of >80% support from trusted validators. If you operate an XRP Ledger server older than version 2.2.0, upgrade by August 14, 2024 to ensure service continuity. The exact time that protocol changes take effect depends on the voting decisions of the decentralized network. Version 2.2.1 is recommended because of known bugs affecting stability of versions 2.2.0. - -If you operate a Clio server, Clio needs to be updated to 2.2.2 before updating to rippled 2.2.1. Clio will be blocked if it is not updated. - -## Changelog - -### Amendments and New Features - -- None - -### Bug Fixes and Performance Improvements - -- Improve error handling in some RPC commands. [#5078](https://github.com/XRPLF/rippled/pull/5078) - -- Use error codes throughout fast Base58 implementation. [#5078](https://github.com/XRPLF/rippled/pull/5078) - -### Docs and Build System - -- None - -### GitHub - -The public source code repository for `rippled` is hosted on GitHub at . - -We welcome all contributions and invite everyone to join the community of XRP Ledger developers to help build the Internet of Value. - - -## Credits - -The following people contributed directly to this release: - -John Freeman -Mayukha Vadari - -Bug Bounties and Responsible Disclosures: - -We welcome reviews of the `rippled` code and urge researchers to responsibly disclose any issues they may find. - -To report a bug, please send a detailed report to: - - -# Version 2.2.0 - -Version 2.2.0 of `rippled`, the reference server implementation of the XRP Ledger protocol, is now available. This release adds performance optimizations, several bug fixes, and introduces the `featurePriceOracle`, `fixEmptyDID`, `fixXChainRewardRounding`, `fixPreviousTxnID`, and `fixAMMv1_1` amendments. - -[Sign Up for Future Release Announcements](https://groups.google.com/g/ripple-server) - - - -## Action Required - -Five new amendments are now open for voting according to the XRP Ledger's [amendment process](https://xrpl.org/amendments.html), which enables protocol changes following two weeks of >80% support from trusted validators. - -If you operate an XRP Ledger server, upgrade to version 2.2.0 by June 17, 2024 to ensure service continuity. The exact time that protocol changes take effect depends on the voting decisions of the decentralized network. - -If you operate a Clio server, Clio needs to be updated to 2.1.2 before updating to rippled 2.2.0. Clio will be blocked if it is not updated. - -## Changelog - -### Amendments and New Features -(These are changes which may impact or be useful to end users. For example, you may be able to update your code/workflow to take advantage of these changes.) - -- **featurePriceOracle** amendment: Implements a price oracle as defined in the [XLS-47](https://github.com/XRPLF/XRPL-Standards/blob/master/XLS-47d-PriceOracles/README.md) spec. A Price Oracle is used to bring real-world data, such as market prices, onto the blockchain, enabling dApps to access and utilize information that resides outside the blockchain. [#4789](https://github.com/XRPLF/rippled/pull/4789) - -- **fixEmptyDID** amendment: Modifies the behavior of the DID amendment: adds an additional check to ensure that DIDs are non-empty when created, and returns a `tecEMPTY_DID` error if the DID would be empty. [#4950](https://github.com/XRPLF/rippled/pull/4950) - -- **fixXChainRewardRounding** amendment: Modifies the behavior of the XChainBridge amendment: fixes rounding so reward shares are always rounded down, even when the `fixUniversalNumber` amendment is active. [#4933](https://github.com/XRPLF/rippled/pull/4933) - -- **fixPreviousTxnID** amendment: Adds `PreviousTxnID` and `PreviousTxnLgrSequence` as fields to all ledger entries that did not already have them included (`DirectoryNode`, `Amendments`, `FeeSettings`, `NegativeUNL`, and `AMM`). Existing ledger entries will gain the fields whenever transactions modify those entries. [#4751](https://github.com/XRPLF/rippled/pull/4751). - -- **fixAMMv1_1** amendment: Fixes AMM offer rounding and low quality order book offers from blocking the AMM. [#4983](https://github.com/XRPLF/rippled/pull/4983) - -- Add a non-admin version of `feature` API method. [#4781](https://github.com/XRPLF/rippled/pull/4781) - -### Bug Fixes and Performance Improvements -(These are behind-the-scenes improvements, such as internal changes to the code, which are not expected to impact end users.) - -- Optimize the base58 encoder and decoder. The algorithm is now about 10 times faster for encoding and 15 times faster for decoding. [#4327](https://github.com/XRPLF/rippled/pull/4327) - -- Optimize the `account_tx` SQL query. [#4955](https://github.com/XRPLF/rippled/pull/4955) - -- Don't reach consensus as quickly if no other proposals are seen. [#4763](https://github.com/XRPLF/rippled/pull/4763) - -- Fix a potential deadlock in the database module. [#4989](https://github.com/XRPLF/rippled/pull/4989) - -- Enforce no duplicate slots from incoming connections. [#4944](https://github.com/XRPLF/rippled/pull/4944) - -- Fix an order book update variable swap. [#4890](https://github.com/XRPLF/rippled/pull/4890) - -### Docs and Build System - -- Add unit test to raise the test coverage of the AMM. [#4971](https://github.com/XRPLF/rippled/pull/4971) - -- Improve test coverage reporting. [#4977](https://github.com/XRPLF/rippled/pull/4977) - -### GitHub - -The public source code repository for `rippled` is hosted on GitHub at . - -We welcome all contributions and invite everyone to join the community of XRP Ledger developers to help build the Internet of Value. - - -## Credits - -The following people contributed directly to this release: - -Alex Kremer -Alloy Networks <45832257+alloynetworks@users.noreply.github.com> -Bronek Kozicki -Chenna Keshava -Denis Angell -Ed Hennis -Gregory Tsipenyuk -Howard Hinnant -John Freeman -Mark Travis -Mayukha Vadari -Michael Legleux -Nik Bougalis -Olek <115580134+oleks-rip@users.noreply.github.com> -Scott Determan -Snoppy - -Bug Bounties and Responsible Disclosures: - -We welcome reviews of the `rippled` code and urge researchers to responsibly disclose any issues they may find. - -To report a bug, please send a detailed report to: - - -## Version 2.1.1 - -The `rippled` 2.1.1 release fixes a critical bug in the integration of AMMs with the payment engine. - -[Sign Up for Future Release Announcements](https://groups.google.com/g/ripple-server) - - - - -## Action Required - -One new amendment is now open for voting according to the XRP Ledger's [amendment process](https://xrpl.org/amendments.html), which enables protocol changes following two weeks of >80% support from trusted validators. - -If you operate an XRP Ledger server, upgrade to version 2.1.1 by April 8, 2024 to ensure service continuity. The exact time that protocol changes take effect depends on the voting decisions of the decentralized network. - -## Changelog - -### Amendments - -- **fixAMMOverflowOffer**: Fix improper handling of large synthetic AMM offers in the payment engine. Due to the importance of this fix, the default vote in the source code has been set to YES. For information on how to configure your validator's amendment voting, see [Configure Amendment Voting](https://xrpl.org/docs/infrastructure/configuration/configure-amendment-voting). - -# Introducing XRP Ledger version 2.1.0 - -Version 2.1.0 of `rippled`, the reference server implementation of the XRP Ledger protocol, is now available. This release adds a bug fix, build improvements, and introduces the `fixNFTokenReserve` and `fixInnerObjTemplate` amendments. - -[Sign Up for Future Release Announcements](https://groups.google.com/g/ripple-server) - - - - -## Action Required - -Two new amendments are now open for voting according to the XRP Ledger's [amendment process](https://xrpl.org/amendments.html), which enables protocol changes following two weeks of >80% support from trusted validators. - -If you operate an XRP Ledger server, upgrade to version 2.1.0 by March 5, 2024 to ensure service continuity. The exact time that protocol changes take effect depends on the voting decisions of the decentralized network. - -## Changelog - -### Amendments -(These are changes which may impact or be useful to end users. For example, you may be able to update your code/workflow to take advantage of these changes.) - -- **fixNFTokenReserve**: Adds a check to the `NFTokenAcceptOffer` transactor to see if the `OwnerCount` changed. If it did, it checks that the reserve requirement is met. [#4767](https://github.com/XRPLF/rippled/pull/4767) - -- **fixInnerObjTemplate**: Adds an `STObject` constructor overload that includes an additional boolean argument to set the inner object template; currently, the inner object template isn't set upon object creation. In some circumstances, this causes a `tefEXCEPTION` error when trying to access the AMM `sfTradingFee` and `sfDiscountedFee` fields in the inner objects of `sfVoteEntry` and `sfAuctionSlot`. [#4906](https://github.com/XRPLF/rippled/pull/4906) - - -### Bug Fixes and Performance Improvements -(These are behind-the-scenes improvements, such as internal changes to the code, which are not expected to impact end users.) - -- Fixed a bug that prevented the gRPC port info from being specified in the `rippled` config file. [#4728](https://github.com/XRPLF/rippled/pull/4728) - - -### Docs and Build System - -- Added unit tests to check that payees and payers aren't the same account. [#4860](https://github.com/XRPLF/rippled/pull/4860) - -- Removed a workaround that bypassed Windows CI unit test failures. [#4871](https://github.com/XRPLF/rippled/pull/4871) - -- Updated library names to be platform-agnostic in Conan recipes. [#4831](https://github.com/XRPLF/rippled/pull/4831) - -- Added headers required in the Conan package to build xbridge witness servers. [#4885](https://github.com/XRPLF/rippled/pull/4885) - -- Improved object lifetime management when creating a temporary `Rules` object, fixing a crash in Windows unit tests. [#4917](https://github.com/XRPLF/rippled/pull/4917) - -### GitHub - -The public source code repository for `rippled` is hosted on GitHub at . - -We welcome all contributions and invite everyone to join the community of XRP Ledger developers to help build the Internet of Value. - - -## Credits - -The following people contributed directly to this release: - -- Bronek Kozicki -- CJ Cobb -- Chenna Keshava B S <21219765+ckeshava@users.noreply.github.com> -- Ed Hennis -- Elliot Lee -- Gregory Tsipenyuk -- John Freeman -- Michael Legleux -- Ryan Molley -- Shawn Xie <35279399+shawnxie999@users.noreply.github.com> - - -Bug Bounties and Responsible Disclosures: - -We welcome reviews of the `rippled` code and urge researchers to responsibly disclose any issues they may find. - -To report a bug, please send a detailed report to: - -# Introducing XRP Ledger version 2.0.1 - -Version 2.0.1 of `rippled`, the reference server implementation of the XRP Ledger protocol, is now available. This release includes minor fixes, unit test improvements, and doc updates. - -[Sign Up for Future Release Announcements](https://groups.google.com/g/ripple-server) - - - - -## Action Required - -If you operate an XRP Ledger server, upgrade to version 2.0.1 to take advantage of the changes included in this update. Nodes on version 1.12 should upgrade as soon as possible. - - -## Changelog - - -### Changes -(These are changes which may impact or be useful to end users. For example, you may be able to update your code/workflow to take advantage of these changes.) - -- Updated the `send_queue_limit` to 500 in the default `rippled` config to handle increased transaction loads. [#4867](https://github.com/XRPLF/rippled/pull/4867) - - -### Bug Fixes and Performance Improvements -(These are behind-the-scenes improvements, such as internal changes to the code, which are not expected to impact end users.) - -- Fixed an assertion that occurred when `rippled` was under heavy websocket client load. [#4848](https://github.com/XRPLF/rippled/pull/4848) - -- Improved lifetime management of serialized type ledger entries to improve memory usage. [#4822](https://github.com/XRPLF/rippled/pull/4822) - -- Fixed a clang warning about deprecated sprintf usage. [#4747](https://github.com/XRPLF/rippled/pull/4747) - - -### Docs and Build System - -- Added `DeliverMax` to more JSONRPC tests. [#4826](https://github.com/XRPLF/rippled/pull/4826) - -- Updated the pull request template to include a `Type of Change` checkbox and additional contextual questions. [#4875](https://github.com/XRPLF/rippled/pull/4875) - -- Updated help messages for unit tests pattern matching. [#4846](https://github.com/XRPLF/rippled/pull/4846) - -- Improved the time it take to generate coverage reports. [#4849](https://github.com/XRPLF/rippled/pull/4849) - -- Fixed broken links in the Conan build docs. [#4699](https://github.com/XRPLF/rippled/pull/4699) - -- Spurious codecov uploads are now retried if there's an error uploading them the first time. [#4896](https://github.com/XRPLF/rippled/pull/4896) - - -### GitHub - -The public source code repository for `rippled` is hosted on GitHub at . - -We welcome all contributions and invite everyone to join the community of XRP Ledger developers to help build the Internet of Value. - - -## Credits - -The following people contributed directly to this release: - -- Bronek Kozicki -- Chenna Keshava B S <21219765+ckeshava@users.noreply.github.com> -- Ed Hennis -- Elliot Lee -- Lathan Britz -- Mark Travis -- nixer89 - -Bug Bounties and Responsible Disclosures: - -We welcome reviews of the `rippled` code and urge researchers to responsibly disclose any issues they may find. - -To report a bug, please send a detailed report to: - -# Introducing XRP Ledger version 2.0.0 - -Version 2.0.0 of `rippled`, the reference server implementation of the XRP Ledger protocol, is now available. This release adds new features and bug fixes, and introduces these amendments: - -- `DID` -- `XChainBridge` -- `fixDisallowIncomingV1` -- `fixFillOrKill` - -[Sign Up for Future Release Announcements](https://groups.google.com/g/ripple-server) - - - - -## Action Required - -Four new amendments are now open for voting according to the XRP Ledger's [amendment process](https://xrpl.org/amendments.html), which enables protocol changes following two weeks of >80% support from trusted validators. - -If you operate an XRP Ledger server, upgrade to version 2.0.0 by January 22, 2024 to ensure service continuity. The exact time that protocol changes take effect depends on the voting decisions of the decentralized network. - - -## Changelog - - -### Amendments, New Features, and Changes -(These are changes which may impact or be useful to end users. For example, you may be able to update your code/workflow to take advantage of these changes.) - -- **XChainBridge**: Introduces cross-chain bridges, enabling interoperability between the XRP Ledger and sidechains. [#4292](https://github.com/XRPLF/rippled/pull/4292) - -- **DID**: Introduces decentralized identifiers. [#4636](https://github.com/XRPLF/rippled/pull/4636) - -- **fixDisallowIncomingV1**: Fixes an issue that occurs when users try to authorize a trustline while the `lsfDisallowIncomingTrustline` flag is enabled on their account. [#4721](https://github.com/XRPLF/rippled/pull/4721) - -- **fixFillOrKill**: Fixes an issue introduced in the `flowCross` amendment. The `tfFillOrKill` and `tfSell` flags are now properly handled to allow offers to cross in certain scenarios. [#4694](https://github.com/XRPLF/rippled/pull/4694) - -- **API v2 released with these changes:** - - - Accepts currency codes in ASCII, using the full alphabet. [#4566](https://github.com/XRPLF/rippled/pull/4566) - - Added test to verify the `check` field is a string. [#4630](https://github.com/XRPLF/rippled/pull/4630) - - Added errors for malformed `account_tx` and `noripple_check` fields. [#4620](https://github.com/XRPLF/rippled/pull/4620) - - Added errors for malformed `gateway_balances` and `channel_authorize` requests. [#4618](https://github.com/XRPLF/rippled/pull/4618) - - Added a `DeliverMax` alias to `Amount` and removed `Amount`. [#4733](https://github.com/XRPLF/rippled/pull/4733) - - Removed `tx_history` and `ledger_header` methods. Also updated `RPC::Handler` to allow for version-specific methods. [#4759](https://github.com/XRPLF/rippled/pull/4759) - - Standardized the JSON serialization format of transactions. [#4727](https://github.com/XRPLF/rippled/issues/4727) - - Bumped API support to v2, but kept the command-line interface for `rippled` and unit tests at v1. [#4803](https://github.com/XRPLF/rippled/pull/4803) - - Standardized `ledger_index` to return as a number. [#4820](https://github.com/XRPLF/rippled/pull/4820) - -- Added a `server_definitions` command that returns an SDK-compatible `definitions.json` file, generated from the `rippled` instance currently running. [#4703](https://github.com/XRPLF/rippled/pull/4703) - -- Improved unit test command line input and run times. [#4634](https://github.com/XRPLF/rippled/pull/4634) - -- Added the link compression setting to the the `rippled-example.cfg` file. [#4753](https://github.com/XRPLF/rippled/pull/4753) - -- Changed the reserved hook error code name from `tecHOOK_ERROR` to `tecHOOK_REJECTED`. [#4559](https://github.com/XRPLF/rippled/pull/4559) - - -### Bug Fixes and Performance Improvements -(These are behind-the-scenes improvements, such as internal changes to the code, which are not expected to impact end users.) - -- Simplified `TxFormats` common fields logic. [#4637](https://github.com/XRPLF/rippled/pull/4637) - -- Improved transaction throughput by asynchronously writing batches to *NuDB*. [#4503](https://github.com/XRPLF/rippled/pull/4503) - -- Removed 2 unused functions. [#4708](https://github.com/XRPLF/rippled/pull/4708) - -- Removed an unused variable that caused clang 14 build errors. [#4672](https://github.com/XRPLF/rippled/pull/4672) - -- Fixed comment about return value of `LedgerHistory::fixIndex`. [#4574](https://github.com/XRPLF/rippled/pull/4574) - -- Updated `secp256k1` to 0.3.2. [#4653](https://github.com/XRPLF/rippled/pull/4653) - -- Removed built-in SNTP clock issues. [#4628](https://github.com/XRPLF/rippled/pull/4628) - -- Fixed amendment flapping. This issue usually occurred when an amendment was on the verge of gaining majority, but a validator not in favor of the amendment went offline. [#4410](https://github.com/XRPLF/rippled/pull/4410) - -- Fixed asan stack-use-after-scope issue. [#4676](https://github.com/XRPLF/rippled/pull/4676) - -- Transactions and pseudo-transactions share the same `commonFields` again. [#4715](https://github.com/XRPLF/rippled/pull/4715) - -- Reduced boilerplate in `applySteps.cpp`. When a new transactor is added, only one function needs to be modified now. [#4710](https://github.com/XRPLF/rippled/pull/4710) - -- Removed an incorrect assert. [#4743](https://github.com/XRPLF/rippled/pull/4743) - -- Replaced some asserts in `PeerFinder::Logic` with `LogicError` to better indicate the nature of server crashes. [#4562](https://github.com/XRPLF/rippled/pull/4562) - -- Fixed an issue with enabling new amendments on a network with an ID greater than 1024. [#4737](https://github.com/XRPLF/rippled/pull/4737) - - -### Docs and Build System - -- Updated `rippled-example.cfg` docs to clarify usage of *ssl_cert* vs *ssl_chain*. [#4667](https://github.com/XRPLF/rippled/pull/4667) - -- Updated `BUILD.md`: - - Made the `environment.md` link easier to find. Also made it easier to find platform-specific info. [#4507](https://github.com/XRPLF/rippled/pull/4507) - - Fixed typo. [#4718](https://github.com/XRPLF/rippled/pull/4718) - - Updated the minimum compiler requirements. [#4700](https://github.com/XRPLF/rippled/pull/4700) - - Added note about enabling `XRPFees`. [#4741](https://github.com/XRPLF/rippled/pull/4741) - -- Updated `API-CHANGELOG.md`: - - Explained API v2 is releasing with `rippled` 2.0.0. [#4633](https://github.com/XRPLF/rippled/pull/4633) - - Clarified the location of the `signer_lists` field in the `account_info` response for API v2. [#4724](https://github.com/XRPLF/rippled/pull/4724) - - Added documentation for the new `DeliverMax` field. [#4784](https://github.com/XRPLF/rippled/pull/4784) - - Removed references to API v2 being "in progress" and "in beta". [#4828](https://github.com/XRPLF/rippled/pull/4828) - - Clarified that all breaking API changes will now occur in API v3 or later. [#4773](https://github.com/XRPLF/rippled/pull/4773) - -- Fixed a mistake in the overlay README. [#4635](https://github.com/XRPLF/rippled/pull/4635) - -- Fixed an early return from `RippledRelease.cmake` that prevented targets from being created during packaging. [#4707](https://github.com/XRPLF/rippled/pull/4707) - -- Fixed a build error with Intel Macs. [#4632](https://github.com/XRPLF/rippled/pull/4632) - -- Added `.build` to `.gitignore`. [#4722](https://github.com/XRPLF/rippled/pull/4722) - -- Fixed a `uint is not universally defined` Windows build error. [#4731](https://github.com/XRPLF/rippled/pull/4731) - -- Reenabled Windows CI build with Artifactory support. [#4596](https://github.com/XRPLF/rippled/pull/4596) - -- Fixed output of remote step in Nix workflow. [#4746](https://github.com/XRPLF/rippled/pull/4746) - -- Fixed a broken link in `conan.md`. [#4740](https://github.com/XRPLF/rippled/pull/4740) - -- Added a `python` call to fix the `pip` upgrade command in Windows CI. [#4768](https://github.com/XRPLF/rippled/pull/4768) - -- Added an API Impact section to `pull_request_template.md`. [#4757](https://github.com/XRPLF/rippled/pull/4757) - -- Set permissions for the Doxygen workflow. [#4756](https://github.com/XRPLF/rippled/pull/4756) - -- Switched to Unity builds to speed up Windows CI. [#4780](https://github.com/XRPLF/rippled/pull/4780) - -- Clarified what makes consensus healthy in `FeeEscalation.md`. [#4729](https://github.com/XRPLF/rippled/pull/4729) - -- Removed a dependency on the header for unit tests. [#4788](https://github.com/XRPLF/rippled/pull/4788) - -- Fixed a clang `unused-but-set-variable` warning. [#4677](https://github.com/XRPLF/rippled/pull/4677) - -- Removed an unused Dockerfile. [#4791](https://github.com/XRPLF/rippled/pull/4791) - -- Fixed unit tests to work with API v2. [#4785](https://github.com/XRPLF/rippled/pull/4785) - -- Added support for the mold linker on Linux. [#4807](https://github.com/XRPLF/rippled/pull/4807) - -- Updated Linux distribtuions `rippled` smoke tests run on. [#4813](https://github.com/XRPLF/rippled/pull/4813) - -- Added codename `bookworm` to the distribution matrix during Artifactory uploads, enabling Debian 12 clients to install `rippled` packages. [#4836](https://github.com/XRPLF/rippled/pull/4836) - -- Added a workaround for compilation errors with GCC 13 and other compilers relying on libstdc++ version 13. [#4817](https://github.com/XRPLF/rippled/pull/4817) - -- Fixed a minor typo in the code comments of `AMMCreate.h`. [4821](https://github.com/XRPLF/rippled/pull/4821) - - -### GitHub - -The public source code repository for `rippled` is hosted on GitHub at . - -We welcome all contributions and invite everyone to join the community of XRP Ledger developers to help build the Internet of Value. - - -## Credits - -The following people contributed directly to this release: - -- Bronek Kozicki -- Chenna Keshava B S <21219765+ckeshava@users.noreply.github.com> -- Denis Angell -- Ed Hennis -- Elliot Lee -- Florent <36513774+florent-uzio@users.noreply.github.com> -- ForwardSlashBack <142098649+ForwardSlashBack@users.noreply.github.com> -- Gregory Tsipenyuk -- Howard Hinnant -- Hussein Badakhchani -- Jackson Mills -- John Freeman -- Manoj Doshi -- Mark Pevec -- Mark Travis -- Mayukha Vadari -- Michael Legleux -- Nik Bougalis -- Peter Chen <34582813+PeterChen13579@users.noreply.github.com> -- Rome Reginelli -- Scott Determan -- Scott Schurr -- Sophia Xie <106177003+sophiax851@users.noreply.github.com> -- Stefan van Kessel -- pwang200 <354723+pwang200@users.noreply.github.com> -- shichengsg002 <147461171+shichengsg002@users.noreply.github.com> -- sokkaofthewatertribe <140777955+sokkaofthewatertribe@users.noreply.github.com> - -Bug Bounties and Responsible Disclosures: - -We welcome reviews of the rippled code and urge researchers to responsibly disclose any issues they may find. - -To report a bug, please send a detailed report to: - - -# Introducing XRP Ledger version 1.12.0 - -Version 1.12.0 of `rippled`, the reference server implementation of the XRP Ledger protocol, is now available. This release adds new features and bug fixes, and introduces these amendments: - -- `AMM` -- `Clawback` -- `fixReducedOffersV1` - -[Sign Up for Future Release Announcements](https://groups.google.com/g/ripple-server) - - - -## Action Required - -Three new amendments are now open for voting according to the XRP Ledger's [amendment process](https://xrpl.org/amendments.html), which enables protocol changes following two weeks of >80% support from trusted validators. - -If you operate an XRP Ledger server, upgrade to version 1.12.0 by September 20, 2023 to ensure service continuity. The exact time that protocol changes take effect depends on the voting decisions of the decentralized network. - - -## Install / Upgrade - -On supported platforms, see the [instructions on installing or updating `rippled`](https://xrpl.org/install-rippled.html). - -The XRPL Foundation publishes portable binaries, which are drop-in replacements for the `rippled` daemon. [See information and downloads for the portable binaries](https://github.com/XRPLF/rippled-portable-builds#portable-builds-of-the-rippled-server). This will work on most distributions, including Ubuntu 16.04, 18.04, 20.04, and 22.04; CentOS; and others. Please test and open issues on GitHub if there are problems. - - -## Changelog - -### Amendments, New Features, and Changes -(These are changes which may impact or be useful to end users. For example, you may be able to update your code/workflow to take advantage of these changes.) - -- **`AMM`**: Introduces an automated market maker (AMM) protocol to the XRP Ledger's decentralized exchange, enabling you to trade assets without a counterparty. For more information about AMMs, see: [Automated Market Maker](https://opensource.ripple.com/docs/xls-30d-amm/amm-uc/). [#4294](https://github.com/XRPLF/rippled/pull/4294) - -- **`Clawback`**: Adds a setting, *Allow Clawback*, which lets an issuer recover, or _claw back_, tokens that they previously issued. Issuers cannot enable this setting if they have issued tokens already. For additional documentation on this feature, see: [#4553](https://github.com/XRPLF/rippled/pull/4553). - -- **`fixReducedOffersV1`**: Reduces the occurrence of order books that are blocked by reduced offers. [#4512](https://github.com/XRPLF/rippled/pull/4512) - -- Added WebSocket and RPC port info to `server_info` responses. [#4427](https://github.com/XRPLF/rippled/pull/4427) - -- Removed the deprecated `accepted`, `seqNum`, `hash`, and `totalCoins` fields from the `ledger` method. [#4244](https://github.com/XRPLF/rippled/pull/4244) - - -### Bug Fixes and Performance Improvements -(These are behind-the-scenes improvements, such as internal changes to the code, which are not expected to impact end users.) - -- Added a pre-commit hook that runs the clang-format linter locally before committing changes. To install this feature, see: [CONTRIBUTING](https://github.com/XRPLF/xrpl-dev-portal/blob/master/CONTRIBUTING.md). [#4599](https://github.com/XRPLF/rippled/pull/4599) - -- In order to make it more straightforward to catch and handle overflows: changed the output type of the `mulDiv()` function from `std::pair` to `std::optional`. [#4243](https://github.com/XRPLF/rippled/pull/4243) - -- Updated `Handler::Condition` enum values to make the code less brittle. [#4239](https://github.com/XRPLF/rippled/pull/4239) - -- Renamed `ServerHandlerImp` to `ServerHandler`. [#4516](https://github.com/XRPLF/rippled/pull/4516), [#4592](https://github.com/XRPLF/rippled/pull/4592) - -- Replaced hand-rolled code with `std::from_chars` for better maintainability. [#4473](https://github.com/XRPLF/rippled/pull/4473) - -- Removed an unused `TypedField` move constructor. [#4567](https://github.com/XRPLF/rippled/pull/4567) - - -### Docs and Build System - -- Updated checkout versions to resolve warnings during GitHub jobs. [#4598](https://github.com/XRPLF/rippled/pull/4598) - -- Fixed an issue with the Debian package build. [#4591](https://github.com/XRPLF/rippled/pull/4591) - -- Updated build instructions with additional steps to take after updating dependencies. [#4623](https://github.com/XRPLF/rippled/pull/4623) - -- Updated contributing doc to clarify that beta releases should also be pushed to the `release` branch. [#4589](https://github.com/XRPLF/rippled/pull/4589) - -- Enabled the `BETA_RPC_API` flag in the default unit tests config, making the API v2 (beta) available to unit tests. [#4573](https://github.com/XRPLF/rippled/pull/4573) - -- Conan dependency management. - - Fixed package definitions for Conan. [#4485](https://github.com/XRPLF/rippled/pull/4485) - - Updated build dependencies to the most recent versions in Conan Center. [#4595](https://github.com/XRPLF/rippled/pull/4595) - - Updated Conan recipe for NuDB. [#4615](https://github.com/XRPLF/rippled/pull/4615) - -- Added binary hardening and linker flags to enhance security during the build process. [#4603](https://github.com/XRPLF/rippled/pull/4603) - -- Added an Artifactory to the `nix` workflow to improve build times. [#4556](https://github.com/XRPLF/rippled/pull/4556) - -- Added quality-of-life improvements to workflows, using new [concurrency control](https://docs.github.com/en/actions/using-jobs/using-concurrency) features. [#4597](https://github.com/XRPLF/rippled/pull/4597) - - -[Full Commit Log](https://github.com/XRPLF/rippled/compare/1.11.0...1.12.0) - - -### GitHub - -The public source code repository for `rippled` is hosted on GitHub at . - -We welcome all contributions and invite everyone to join the community of XRP Ledger developers to help build the Internet of Value. - - -## Credits - -The following people contributed directly to this release: - -- Alphonse N. Mousse <39067955+a-noni-mousse@users.noreply.github.com> -- Arihant Kothari -- Chenna Keshava B S <21219765+ckeshava@users.noreply.github.com> -- Denis Angell -- Ed Hennis -- Elliot Lee -- Gregory Tsipenyuk -- Howard Hinnant -- Ikko Eltociear Ashimine -- John Freeman -- Manoj Doshi -- Mark Travis -- Mayukha Vadari -- Michael Legleux -- Peter Chen <34582813+PeterChen13579@users.noreply.github.com> -- RichardAH -- Rome Reginelli -- Scott Schurr -- Shawn Xie <35279399+shawnxie999@users.noreply.github.com> -- drlongle - -Bug Bounties and Responsible Disclosures: - -We welcome reviews of the rippled code and urge researchers to responsibly disclose any issues they may find. - -To report a bug, please send a detailed report to: - - -# Introducing XRP Ledger version 1.11.0 - -Version 1.11.0 of `rippled`, the reference server implementation of the XRP Ledger protocol, is now available. - -This release reduces memory usage, introduces the `fixNFTokenRemint` amendment, and adds new features and bug fixes. For example, the new NetworkID field in transactions helps to prevent replay attacks with side-chains. - -[Sign Up for Future Release Announcements](https://groups.google.com/g/ripple-server) - - - -## Action Required - -The `fixNFTokenRemint` amendment is now open for voting according to the XRP Ledger's [amendment process](https://xrpl.org/amendments.html), which enables protocol changes following two weeks of >80% support from trusted validators. - -If you operate an XRP Ledger server, upgrade to version 1.11.0 by July 5 to ensure service continuity. The exact time that protocol changes take effect depends on the voting decisions of the decentralized network. - - -## Install / Upgrade - -On supported platforms, see the [instructions on installing or updating `rippled`](https://xrpl.org/install-rippled.html). - - -## What's Changed - -### New Features and Improvements - -* Allow port numbers be be specified using a either a colon or a space by @RichardAH in https://github.com/XRPLF/rippled/pull/4328 -* Eliminate memory allocation from critical path: by @nbougalis in https://github.com/XRPLF/rippled/pull/4353 -* Make it easy for projects to depend on libxrpl by @thejohnfreeman in https://github.com/XRPLF/rippled/pull/4449 -* Add the ability to mark amendments as obsolete by @ximinez in https://github.com/XRPLF/rippled/pull/4291 -* Always create the FeeSettings object in genesis ledger by @ximinez in https://github.com/XRPLF/rippled/pull/4319 -* Log exception messages in several locations by @drlongle in https://github.com/XRPLF/rippled/pull/4400 -* Parse flags in account_info method by @drlongle in https://github.com/XRPLF/rippled/pull/4459 -* Add NFTokenPages to account_objects RPC by @RichardAH in https://github.com/XRPLF/rippled/pull/4352 -* add jss fields used by clio `nft_info` by @ledhed2222 in https://github.com/XRPLF/rippled/pull/4320 -* Introduce a slab-based memory allocator and optimize SHAMapItem by @nbougalis in https://github.com/XRPLF/rippled/pull/4218 -* Add NetworkID field to transactions to help prevent replay attacks on and from side-chains by @RichardAH in https://github.com/XRPLF/rippled/pull/4370 -* If present, set quorum based on command line. by @mtrippled in https://github.com/XRPLF/rippled/pull/4489 -* API does not accept seed or public key for account by @drlongle in https://github.com/XRPLF/rippled/pull/4404 -* Add `nftoken_id`, `nftoken_ids` and `offer_id` meta fields into NFT `Tx` responses by @shawnxie999 in https://github.com/XRPLF/rippled/pull/4447 - -### Bug Fixes - -* fix(gateway_balances): handle overflow exception by @RichardAH in https://github.com/XRPLF/rippled/pull/4355 -* fix(ValidatorSite): handle rare null pointer dereference in timeout by @ximinez in https://github.com/XRPLF/rippled/pull/4420 -* RPC commands understand markers derived from all ledger object types by @ximinez in https://github.com/XRPLF/rippled/pull/4361 -* `fixNFTokenRemint`: prevent NFT re-mint: by @shawnxie999 in https://github.com/XRPLF/rippled/pull/4406 -* Fix a case where ripple::Expected returned a json array, not a value by @scottschurr in https://github.com/XRPLF/rippled/pull/4401 -* fix: Ledger data returns an empty list (instead of null) when all entries are filtered out by @drlongle in https://github.com/XRPLF/rippled/pull/4398 -* Fix unit test ripple.app.LedgerData by @drlongle in https://github.com/XRPLF/rippled/pull/4484 -* Fix the fix for std::result_of by @thejohnfreeman in https://github.com/XRPLF/rippled/pull/4496 -* Fix errors for Clang 16 by @thejohnfreeman in https://github.com/XRPLF/rippled/pull/4501 -* Ensure that switchover vars are initialized before use: by @seelabs in https://github.com/XRPLF/rippled/pull/4527 -* Move faulty assert by @ximinez in https://github.com/XRPLF/rippled/pull/4533 -* Fix unaligned load and stores: (#4528) by @seelabs in https://github.com/XRPLF/rippled/pull/4531 -* fix node size estimation by @dangell7 in https://github.com/XRPLF/rippled/pull/4536 -* fix: remove redundant moves by @ckeshava in https://github.com/XRPLF/rippled/pull/4565 - -### Code Cleanup and Testing - -* Replace compare() with the three-way comparison operator in base_uint, Issue and Book by @drlongle in https://github.com/XRPLF/rippled/pull/4411 -* Rectify the import paths of boost::function_output_iterator by @ckeshava in https://github.com/XRPLF/rippled/pull/4293 -* Expand Linux test matrix by @thejohnfreeman in https://github.com/XRPLF/rippled/pull/4454 -* Add patched recipe for SOCI by @thejohnfreeman in https://github.com/XRPLF/rippled/pull/4510 -* Switch to self-hosted runners for macOS by @thejohnfreeman in https://github.com/XRPLF/rippled/pull/4511 -* [TRIVIAL] Add missing includes by @seelabs in https://github.com/XRPLF/rippled/pull/4555 - -### Docs - -* Refactor build instructions by @thejohnfreeman in https://github.com/XRPLF/rippled/pull/4381 -* Add install instructions for package managers by @thejohnfreeman in https://github.com/XRPLF/rippled/pull/4472 -* Fix typo by @solmsted in https://github.com/XRPLF/rippled/pull/4508 -* Update environment.md by @sappenin in https://github.com/XRPLF/rippled/pull/4498 -* Update BUILD.md by @oeggert in https://github.com/XRPLF/rippled/pull/4514 -* Trivial: add comments for NFToken-related invariants by @scottschurr in https://github.com/XRPLF/rippled/pull/4558 - -## New Contributors -* @drlongle made their first contribution in https://github.com/XRPLF/rippled/pull/4411 -* @ckeshava made their first contribution in https://github.com/XRPLF/rippled/pull/4293 -* @solmsted made their first contribution in https://github.com/XRPLF/rippled/pull/4508 -* @sappenin made their first contribution in https://github.com/XRPLF/rippled/pull/4498 -* @oeggert made their first contribution in https://github.com/XRPLF/rippled/pull/4514 - -**Full Changelog**: https://github.com/XRPLF/rippled/compare/1.10.1...1.11.0 - - -### GitHub - -The public source code repository for `rippled` is hosted on GitHub at . - -We welcome all contributions and invite everyone to join the community of XRP Ledger developers to help build the Internet of Value. - -### Credits - -The following people contributed directly to this release: -- Alloy Networks <45832257+alloynetworks@users.noreply.github.com> -- Brandon Wilson -- Chenna Keshava B S <21219765+ckeshava@users.noreply.github.com> -- David Fuelling -- Denis Angell -- Ed Hennis -- Elliot Lee -- John Freeman -- Mark Travis -- Nik Bougalis -- RichardAH -- Scott Determan -- Scott Schurr -- Shawn Xie <35279399+shawnxie999@users.noreply.github.com> -- drlongle -- ledhed2222 -- oeggert <117319296+oeggert@users.noreply.github.com> -- solmsted - - -Bug Bounties and Responsible Disclosures: -We welcome reviews of the rippled code and urge researchers to -responsibly disclose any issues they may find. - -To report a bug, please send a detailed report to: - - bugs@xrpl.org - - -# Introducing XRP Ledger version 1.10.1 - -Version 1.10.1 of `rippled`, the reference server implementation of the XRP Ledger protocol, is now available. This release restores packages for Ubuntu 18.04. - -Compared to version 1.10.0, the only C++ code change fixes an edge case in Reporting Mode. - -If you are already running version 1.10.0, then upgrading to version 1.10.1 is generally not required. - -[Sign Up for Future Release Announcements](https://groups.google.com/g/ripple-server) - - - -## Install / Upgrade - -On supported platforms, see the [instructions on installing or updating `rippled`](https://xrpl.org/install-rippled.html). - -## Changelog - -- [`da18c86cbf`](https://github.com/ripple/rippled/commit/da18c86cbfea1d8fe6940035f9103e15890d47ce) Build packages with Ubuntu 18.04 -- [`f7b3ddd87b`](https://github.com/ripple/rippled/commit/f7b3ddd87b8ef093a06ab1420bea57ed1e77643a) Reporting Mode: Do not attempt to acquire missing data from peer network (#4458) - -### GitHub - -The public source code repository for `rippled` is hosted on GitHub at . - -We welcome all contributions and invite everyone to join the community of XRP Ledger developers to help build the Internet of Value. - -### Credits - -The following people contributed directly to this release: - -- John Freeman -- Mark Travis -- Michael Legleux - -Bug Bounties and Responsible Disclosures: -We welcome reviews of the rippled code and urge researchers to -responsibly disclose any issues they may find. - -To report a bug, please send a detailed report to: - - bugs@xrpl.org - - -# Introducing XRP Ledger version 1.10.0 - -Version 1.10.0 of `rippled`, the reference server implementation of the XRP Ledger protocol, is now available. This release introduces six new amendments, detailed below, and cleans up code to improve performance. - -[Sign Up for Future Release Announcements](https://groups.google.com/g/ripple-server) - - - -## Action Required - -Six new amendments are now open for voting according to the XRP Ledger's [amendment process](https://xrpl.org/amendments.html), which enables protocol changes following two weeks of >80% support from trusted validators. - -If you operate an XRP Ledger server, upgrade to version 1.10.0 by March 21 to ensure service continuity. The exact time that protocol changes take effect depends on the voting decisions of the decentralized network. - - -## Install / Upgrade - -On supported platforms, see the [instructions on installing or updating `rippled`](https://xrpl.org/install-rippled.html). - - -## New Amendments - -- **`featureImmediateOfferKilled`**: Changes the response code of an `OfferCreate` transaction with the `tfImmediateOrCancel` flag to return `tecKILLED` when no funds are moved. The previous return code of `tecSUCCESS` was unintuitive. [#4157](https://github.com/XRPLF/rippled/pull/4157) - -- **`featureDisallowIncoming`**: Enables an account to block incoming checks, payment channels, NFToken offers, and trust lines. [#4336](https://github.com/XRPLF/rippled/pull/4336) - -- **`featureXRPFees`**: Simplifies transaction cost calculations to use XRP directly, rather than calculating indirectly in "fee units" and translating the results to XRP. Updates all instances of "fee units" in the protocol and ledger data to be drops of XRP instead. [#4247](https://github.com/XRPLF/rippled/pull/4247) - -- **`fixUniversalNumber`**: Simplifies and unifies the code for decimal floating point math. In some cases, this provides slightly better accuracy than the previous code, resulting in calculations whose least significant digits are different than when calculated with the previous code. The different results may cause other edge case differences where precise calculations are used, such as ranking of offers or processing of payments that use several different paths. [#4192](https://github.com/XRPLF/rippled/pull/4192) - -- **`fixNonFungibleTokensV1_2`**: This amendment is a combination of NFToken fixes. [#4417](https://github.com/XRPLF/rippled/pull/4417) - - Fixes unburnable NFTokens when it has over 500 offers. [#4346](https://github.com/XRPLF/rippled/pull/4346) - - Fixes 3 NFToken offer acceptance issues. [#4380](https://github.com/XRPLF/rippled/pull/4380) - - Prevents brokered sales of NFTokens to owners. [#4403](https://github.com/XRPLF/rippled/pull/4403) - - Only allows the destination to settle NFToken offers through brokerage. [#4399](https://github.com/XRPLF/rippled/pull/4399) - -- **`fixTrustLinesToSelf`**: Trust lines must be between two different accounts, but two exceptions exist because of a bug that briefly existed. This amendment removes those trust lines. [69bb2be](https://github.com/XRPLF/rippled/pull/4270/commits/69bb2be446e3cc24c694c0835b48bd2ecd3d119e) - - -## Changelog - - -### New Features and Improvements - -- **Improve Handshake in the peer protocol**: Switched to using a cryptographically secure PRNG for the Instance Cookie. `rippled` now uses hex encoding for the `Closed-Ledger` and `Previous-Ledger` fields in the Handshake. Also added `--newnodeid` and `--nodeid` command line options. [5a15229](https://github.com/XRPLF/rippled/pull/4270/commits/5a15229eeb13b69c8adf1f653b88a8f8b9480546) - -- **RPC tooBusy response now has 503 HTTP status code**: Added ripplerpc 3.0, enabling RPC tooBusy responses to return relevant HTTP status codes. This is a non-breaking change that only applies to JSON-RPC when you include `"ripplerpc": "3.0"` in the request. [#4143](https://github.com/XRPLF/rippled/pull/4143) - -- **Use the Conan package manager**: Added a `conanfile.py` and Conan recipe for Snappy. Removed the RocksDB recipe from the repo; you can now get it from Conan Center. [#4367](https://github.com/XRPLF/rippled/pull/4367), [c2b03fe](https://github.com/XRPLF/rippled/commit/c2b03fecca19a304b37467b01fa78593d3dce3fb) - -- **Update Build Instructions**: Updated the build instructions to build with the Conan package manager and restructured info for easier comprehension. [#4376](https://github.com/XRPLF/rippled/pull/4376), [#4383](https://github.com/XRPLF/rippled/pull/4383) - -- **Revise CONTRIBUTING**: Updated code contribution guidelines. `rippled` is an open source project and contributions are very welcome. [#4382](https://github.com/XRPLF/rippled/pull/4382) - -- **Update documented pathfinding configuration defaults**: `417cfc2` changed the default Path Finding configuration values, but missed updating the values documented in rippled-example.cfg. Updated those defaults and added recommended values for nodes that want to support advanced pathfinding. [#4409](https://github.com/XRPLF/rippled/pull/4409) - -- **Remove gRPC code previously used for the Xpring SDK**: Removed gRPC code used for the Xpring SDK. The gRPC API is also enabled locally by default in `rippled-example.cfg`. This API is used for [Reporting Mode](https://xrpl.org/build-run-rippled-in-reporting-mode.html) and [Clio](https://github.com/XRPLF/clio). [28f4cc7](https://github.com/XRPLF/rippled/pull/4321/commits/28f4cc7817c2e477f0d7e9ade8f07a45ff2b81f1) - -- **Switch from C++17 to C++20**: Updated `rippled` to use C++20. [92d35e5](https://github.com/XRPLF/rippled/pull/4270/commits/92d35e54c7de6bbe44ff6c7c52cc0765b3f78258) - -- **Support for Boost 1.80.0:**: [04ef885](https://github.com/XRPLF/rippled/pull/4321/commits/04ef8851081f6ee9176783ad3725960b8a931ebb) - -- **Reduce default reserves to 10/2**: Updated the hard-coded default reserves to match the current settings on Mainnet. [#4329](https://github.com/XRPLF/rippled/pull/4329) - -- **Improve self-signed certificate generation**: Improved speed and security of TLS certificate generation on fresh startup. [0ecfc7c](https://github.com/XRPLF/rippled/pull/4270/commits/0ecfc7cb1a958b731e5f184876ea89ae2d4214ee) - - -### Bug Fixes - -- **Update command-line usage help message**: Added `manifest` and `validator_info` to the `rippled` CLI usage statement. [b88ed5a](https://github.com/XRPLF/rippled/pull/4270/commits/b88ed5a8ec2a0735031ca23dc6569d54787dc2f2) - -- **Work around gdb bug by changing a template parameter**: Added a workaround for a bug in gdb, where unsigned template parameters caused issues with RTTI. [#4332](https://github.com/XRPLF/rippled/pull/4332) - -- **Fix clang 15 warnings**: [#4325](https://github.com/XRPLF/rippled/pull/4325) - -- **Catch transaction deserialization error in doLedgerGrpc**: Fixed an issue in the gRPC API, so `Clio` can extract ledger headers and state objects from specific transactions that can't be deserialized by `rippled` code. [#4323](https://github.com/XRPLF/rippled/pull/4323) - -- **Update dependency: gRPC**: New Conan recipes broke the old version of gRPC, so the dependency was updated. [#4407](https://github.com/XRPLF/rippled/pull/4407) - -- **Fix Doxygen workflow**: Added options to build documentation that don't depend on the library dependencies of `rippled`. [#4372](https://github.com/XRPLF/rippled/pull/4372) - -- **Don't try to read SLE with key 0 from the ledger**: Fixed the `preclaim` function to check for 0 in `NFTokenSellOffer` and `NFTokenBuyOffer` before calling `Ledger::read`. This issue only affected debug builds. [#4351](https://github.com/XRPLF/rippled/pull/4351) - -- **Update broken link to hosted Doxygen content**: [5e1cb09](https://github.com/XRPLF/rippled/pull/4270/commits/5e1cb09b8892e650f6c34a66521b6b1673bd6b65) - - -### Code Cleanup - -- **Prevent unnecessary `shared_ptr` copies by accepting a value in `SHAMapInnerNode::setChild`**: [#4266](https://github.com/XRPLF/rippled/pull/4266) - -- **Release TaggedCache object memory outside the lock**: [3726f8b](https://github.com/XRPLF/rippled/pull/4321/commits/3726f8bf31b3eab8bab39dce139656fd705ae9a0) - -- **Rename SHAMapStoreImp::stopping() to healthWait()**: [7e9e910](https://github.com/XRPLF/rippled/pull/4321/commits/7e9e9104eabbf0391a0837de5630af17a788e233) - -- **Improve wrapper around OpenSSL RAND**: [7b3507b](https://github.com/XRPLF/rippled/pull/4270/commits/7b3507bb873495a974db33c57a888221ddabcacc) - -- **Improve AccountID string conversion caching**: Improved memory cache usage. [e2eed96](https://github.com/XRPLF/rippled/pull/4270/commits/e2eed966b0ecb6445027e6a023b48d702c5f4832) - -- **Build the command map at compile time**: [9aaa0df](https://github.com/XRPLF/rippled/pull/4270/commits/9aaa0dff5fd422e5f6880df8e20a1fd5ad3b4424) - -- **Avoid unnecessary copying and dynamic memory allocations**: [d318ab6](https://github.com/XRPLF/rippled/pull/4270/commits/d318ab612adc86f1fd8527a50af232f377ca89ef) - -- **Use constexpr to check memo validity**: [e67f905](https://github.com/XRPLF/rippled/pull/4270/commits/e67f90588a9050162881389d7e7d1d0fb31066b0) - -- **Remove charUnHex**: [83ac141](https://github.com/XRPLF/rippled/pull/4270/commits/83ac141f656b1a95b5661853951ebd95b3ffba99) - -- **Remove deprecated AccountTxOld.cpp**: [ce64f7a](https://github.com/XRPLF/rippled/pull/4270/commits/ce64f7a90f99c6b5e68d3c3d913443023de061a6) - -- **Remove const_cast usage**: [23ce431](https://github.com/XRPLF/rippled/pull/4321/commits/23ce4318768b718c82e01004d23f1abc9a9549ff) - -- **Remove inaccessible code paths and outdated data format wchar_t**: [95fabd5](https://github.com/XRPLF/rippled/pull/4321/commits/95fabd5762a4917753c06268192e4d4e4baef8e4) - -- **Improve move semantics in Expected**: [#4326](https://github.com/XRPLF/rippled/pull/4326) - - -### GitHub - -The public source code repository for `rippled` is hosted on GitHub at . - -We welcome all contributions and invite everyone to join the community of XRP Ledger developers to help build the Internet of Value. - -### Credits - -The following people contributed directly to this release: - -- Alexander Kremer -- Alloy Networks <45832257+alloynetworks@users.noreply.github.com> -- CJ Cobb <46455409+cjcobb23@users.noreply.github.com> -- Chenna Keshava B S -- Crypto Brad Garlinghouse -- Denis Angell -- Ed Hennis -- Elliot Lee -- Gregory Popovitch -- Howard Hinnant -- J. Scott Branson <18340247+crypticrabbit@users.noreply.github.com> -- John Freeman -- ledhed2222 -- Levin Winter <33220502+levinwinter@users.noreply.github.com> -- manojsdoshi -- Nik Bougalis -- RichardAH -- Scott Determan -- Scott Schurr -- Shawn Xie <35279399+shawnxie999@users.noreply.github.com> - -Security Bug Bounty Acknowledgements: -- Aaron Hook -- Levin Winter - -Bug Bounties and Responsible Disclosures: -We welcome reviews of the rippled code and urge researchers to -responsibly disclose any issues they may find. - -To report a bug, please send a detailed report to: - - bugs@xrpl.org - - -# Introducing XRP Ledger version 1.9.4 - -Version 1.9.4 of `rippled`, the reference implementation of the XRP Ledger protocol is now available. This release introduces an amendment that removes the ability for an NFT issuer to indicate that trust lines should be automatically created for royalty payments from secondary sales of NFTs, in response to a bug report that indicated how this functionality could be abused to mount a denial of service attack against the issuer. - -## Action Required - -This release introduces a new amendment to the XRP Ledger protocol, **`fixRemoveNFTokenAutoTrustLine`** to mitigate a potential denial-of-service attack against NFT issuers that minted NFTs and allowed secondary trading of those NFTs to create trust lines for any asset. - -This amendment is open for voting according to the XRP Ledger's [amendment process](https://xrpl.org/amendments.html), which enables protocol changes following two weeks of >80% support from trusted validators. - -If you operate an XRP Ledger server, then you should upgrade to version 1.9.4 within two weeks, to ensure service continuity. The exact time that protocol changes take effect depends on the voting decisions of the decentralized network. - -For more information about NFTs on the XRP Ledger, see [NFT Conceptual Overview](https://xrpl.org/nft-conceptual-overview.html). - - -## Install / Upgrade - -On supported platforms, see the [instructions on installing or updating `rippled`](https://xrpl.org/install-rippled.html). - -## Changelog - -## Contributions - -The primary change in this release is the following bug fix: - -- **Introduce fixRemoveNFTokenAutoTrustLine amendment**: Introduces the `fixRemoveNFTokenAutoTrustLine` amendment, which disables the `tfTrustLine` flag, which a malicious attacker could exploit to mount denial-of-service attacks against NFT issuers that specified the flag on their NFTs. ([#4301](https://github.com/XRPLF/rippled/4301)) - - -### GitHub - -The public source code repository for `rippled` is hosted on GitHub at . - -We welcome all contributions and invite everyone to join the community of XRP Ledger developers and help us build the Internet of Value. - -### Credits - -The following people contributed directly to this release: - -- Scott Schurr -- Howard Hinnant -- Scott Determan -- Ikko Ashimine - - -# Introducing XRP Ledger version 1.9.3 - -Version 1.9.3 of `rippled`, the reference server implementation of the XRP Ledger protocol is now available. This release corrects minor technical flaws with the code that loads configured amendment votes after a startup and the copy constructor of `PublicKey`. - -## Install / Upgrade - -On supported platforms, see the [instructions on installing or updating `rippled`](https://xrpl.org/install-rippled.html). - -## Changelog - -## Contributions - -This release contains the following bug fixes: - -- **Change by-value to by-reference to persist vote**: A minor technical flaw, caused by use of a copy instead of a reference, resulted in operator-configured "yes" votes to not be properly loaded after a restart. ([#4256](https://github.com/XRPLF/rippled/pull/4256)) -- **Properly handle self-assignment of PublicKey**: The `PublicKey` copy assignment operator mishandled the case where a `PublicKey` would be assigned to itself, and could result in undefined behavior. - -### GitHub - -The public source code repository for `rippled` is hosted on GitHub at . - -We welcome contributions, big and small, and invite everyone to join the community of XRP Ledger developers and help us build the Internet of Value. - -### Credits - -The following people contributed directly to this release: - -- Howard Hinnant -- Crypto Brad Garlinghouse -- Wo Jake <87929946+wojake@users.noreply.github.com> - - -# Introducing XRP Ledger version 1.9.2 - -Version 1.9.2 of `rippled`, the reference server implementation of the XRP Ledger protocol, is now available. This release includes several fixes and improvements, including a second new fix amendment to correct a bug in Non-Fungible Tokens (NFTs) code, a new API method for order book changes, less noisy logging, and other small fixes. - - - - -## Action Required - -This release introduces a two new amendments to the XRP Ledger protocol. The first, **fixNFTokenNegOffer**, fixes a bug in code associated with the **NonFungibleTokensV1** amendment, originally introduced in [version 1.9.0](https://xrpl.org/blog/2022/rippled-1.9.0.html). The second, **NonFungibleTokensV1_1**, is a "roll-up" amendment that enables the **NonFungibleTokensV1** feature plus the two fix amendments associated with it, **fixNFTokenDirV1** and **fixNFTokenNegOffer**. - -If you want to enable NFT code on the XRP Ledger Mainnet, you can vote in favor of only the **NonFungibleTokensV1_1** amendment to support enabling the feature and fixes together, without risk that the unfixed NFT code may become enabled first. - -These amendments are now open for voting according to the XRP Ledger's [amendment process](https://xrpl.org/amendments.html), which enables protocol changes following two weeks of >80% support from trusted validators. - -If you operate an XRP Ledger server, then you should upgrade to version 1.9.2 within two weeks, to ensure service continuity. The exact time that protocol changes take effect depends on the voting decisions of the decentralized network. - -For more information about NFTs on the XRP Ledger, see [NFT Conceptual Overview](https://xrpl.org/nft-conceptual-overview.html). - -## Install / Upgrade - -On supported platforms, see the [instructions on installing or updating `rippled`](https://xrpl.org/install-rippled.html). - -## Changelog - -This release contains the following features and improvements. - -- **Introduce fixNFTokenNegOffer amendment.** This amendment fixes a bug in the Non-Fungible Tokens (NFTs) functionality provided by the NonFungibleTokensV1 amendment (not currently enabled on Mainnet). The bug allowed users to place offers to buy tokens for negative amounts of money when using Brokered Mode. Anyone who accepted such an offer would transfer the token _and_ pay money. This amendment explicitly disallows offers to buy or sell NFTs for negative amounts of money, and returns an appropriate error code. This also corrects the error code returned when placing offers to buy or sell NFTs for negative amounts in Direct Mode. ([8266d9d](https://github.com/XRPLF/rippled/commit/8266d9d598d19f05e1155956b30ca443c27e119e)) -- **Introduce `NonFungibleTokensV1_1` amendment.** This amendment encompasses three NFT-related amendments: the original NonFungibleTokensV1 amendment (from version 1.9.0), the fixNFTokenDirV1 amendment (from version 1.9.1), and the new fixNFTokenNegOffer amendment from this release. This amendment contains no changes other than enabling those three amendments together; this allows validators to vote in favor of _only_ enabling the feature and fixes at the same time. ([59326bb](https://github.com/XRPLF/rippled/commit/59326bbbc552287e44b3a0d7b8afbb1ddddb3e3b)) -- **Handle invalid port numbers.** If the user specifies a URL with an invalid port number, the server would silently attempt to use port 0 instead. Now it raises an error instead. This affects admin API methods and config file parameters for downloading history shards and specifying validator list sites. ([#4213](https://github.com/XRPLF/rippled/pull/4213)) -- **Reduce log noisiness.** Decreased the severity of benign log messages in several places: "addPathsForType" messages during regular operation, expected errors during unit tests, and missing optional documentation components when compiling from source. ([#4178](https://github.com/XRPLF/rippled/pull/4178), [#4166](https://github.com/XRPLF/rippled/pull/4166), [#4180](https://github.com/XRPLF/rippled/pull/4180)) -- **Fix race condition in history shard implementation and support clang's ThreadSafetyAnalysis tool.** Added build settings so that developers can use this feature of the clang compiler to analyze the code for correctness, and fix an error found by this tool, which was the source of rare crashes in unit tests. ([#4188](https://github.com/XRPLF/rippled/pull/4188)) -- **Prevent crash when rotating a database with missing data.** When rotating databases, a missing entry could cause the server to crash. While there should never be a missing database entry, this change keeps the server running by aborting database rotation. ([#4182](https://github.com/XRPLF/rippled/pull/4182)) -- **Fix bitwise comparison in OfferCreate.** Fixed an expression that incorrectly used a bitwise comparison for two boolean values rather than a true boolean comparison. The outcome of the two comparisons is equivalent, so this is not a transaction processing change, but the bitwise comparison relied on compilers to implicitly fix the expression. ([#4183](https://github.com/XRPLF/rippled/pull/4183)) -- **Disable cluster timer when not in a cluster.** Disabled a timer that was unused on servers not running in clustered mode. The functionality of clustered servers is unchanged. ([#4173](https://github.com/XRPLF/rippled/pull/4173)) -- **Limit how often to process peer discovery messages.** In the peer-to-peer network, servers periodically share IP addresses of their peers with each other to facilitate peer discovery. It is not necessary to process these types of messages too often; previously, the code tracked whether it needed to process new messages of this type but always processed them anyway. With this change, the server no longer processes peer discovery messages if it has done so recently. ([#4202](https://github.com/XRPLF/rippled/pull/4202)) -- **Improve STVector256 deserialization.** Optimized the processing of this data type in protocol messages. This data type is used in several types of ledger entry that are important for bookkeeping, including directory pages that track other ledger types, amendments tracking, and the ledger hashes history. ([#4204](https://github.com/XRPLF/rippled/pull/4204)) -- **Fix and refactor spinlock code.** The spinlock code, which protects the `SHAMapInnerNode` child lists, had a mistake that allowed the same child to be repeatedly locked under some circumstances. Fixed this bug and improved the spinlock code to make it easier to use correctly and easier to verify that the code works correctly. ([#4201](https://github.com/XRPLF/rippled/pull/4201)) -- **Improve comments and contributor documentation.** Various minor documentation changes including some to reflect the fact that the source code repository is now owned by the XRP Ledger Foundation. ([#4214](https://github.com/XRPLF/rippled/pull/4214), [#4179](https://github.com/XRPLF/rippled/pull/4179), [#4222](https://github.com/XRPLF/rippled/pull/4222)) -- **Introduces a new API book_changes to provide information in a format that is useful for building charts that highlight DEX activity at a per-ledger level.** ([#4212](https://github.com/XRPLF/rippled/pull/4212)) - -## Contributions - -### GitHub - -The public source code repository for `rippled` is hosted on GitHub at . - -We welcome contributions, big and small, and invite everyone to join the community of XRP Ledger developers and help us build the Internet of Value. - -### Credits - -The following people contributed directly to this release: - -- Chenna Keshava B S -- Ed Hennis -- Ikko Ashimine -- Nik Bougalis -- Richard Holland -- Scott Schurr -- Scott Determan - -For a real-time view of all lifetime contributors, including links to the commits made by each, please visit the "Contributors" section of the GitHub repository: . - -# Introducing XRP Ledger version 1.9.1 - -Version 1.9.1 of `rippled`, the reference server implementation of the XRP Ledger protocol, is now available. This release includes several important fixes, including a fix for a syncing issue from 1.9.0, a new fix amendment to correct a bug in the new Non-Fungible Tokens (NFTs) code, and a new amendment to allow multi-signing by up to 32 signers. - - - - -## Action Required - -This release introduces two new amendments to the XRP Ledger protocol. These amendments are now open for voting according to the XRP Ledger's [amendment process](https://xrpl.org/amendments.html), which enables protocol changes following two weeks of >80% support from trusted validators. - -If you operate an XRP Ledger server, then you should upgrade to version 1.9.1 within two weeks, to ensure service continuity. The exact time that protocol changes take effect depends on the voting decisions of the decentralized network. - -The **fixNFTokenDirV1** amendment fixes a bug in code associated with the **NonFungibleTokensV1** amendment, so the fixNFTokenDirV1 amendment should be enabled first. All validator operators are encouraged to [configure amendment voting](https://xrpl.org/configure-amendment-voting.html) to oppose the NonFungibleTokensV1 amendment until _after_ the fixNFTokenDirV1 amendment has become enabled. For more information about NFTs on the XRP Ledger, see [NFT Conceptual Overview](https://xrpl.org/nft-conceptual-overview.html). - -The **ExpandedSignerList** amendment extends the ledger's built-in multi-signing functionality so that each list can contain up to 32 entries instead of the current limit of 8. Additionally, this amendment allows each signer to have an arbitrary 256-bit data field associated with it. This data can be used to identify the signer or provide other metadata that is useful for organizations, smart contracts, or other purposes. - -## Install / Upgrade - -On supported platforms, see the [instructions on installing or updating `rippled`](https://xrpl.org/install-rippled.html). - -## Changelog - -This release contains the following features and improvements. - -## New Features and Amendments - -- **Introduce fixNFTokenDirV1 Amendment** - This amendment fixes an off-by-one error that occurred in some corner cases when determining which `NFTokenPage` an `NFToken` object belongs on. It also adjusts the constraints of `NFTokenPage` invariant checks, so that certain error cases fail with a suitable error code such as `tecNO_SUITABLE_TOKEN_PAGE` instead of failing with a `tecINVARIANT_FAILED` error code. ([#4155](https://github.com/ripple/rippled/pull/4155)) - -- **Introduce ExpandedSignerList Amendment** - This amendment expands the maximum signer list size to 32 entries and allows each signer to have an optional 256-bit `WalletLocator` field containing arbitrary data. ([#4097](https://github.com/ripple/rippled/pull/4097)) - -- **Pause online deletion rather than canceling it if the server fails health check** - The server stops performing online deletion of old ledger history if the server fails its internal health check during this time. Online deletion can now resume after the server recovers, rather than having to start over. ([#4139](https://github.com/ripple/rippled/pull/4139)) - - -## Bug Fixes and Performance Improvements - -- **Fix performance issues introduced in 1.9.0** - Readjusts some parameters of the ledger acquisition engine to revert some changes introduced in 1.9.0 that had adverse effects on some systems, including causing some systems to fail to sync to the network. ([#4152](https://github.com/ripple/rippled/pull/4152)) - -- **Improve Memory Efficiency of Path Finding** - Finding paths for cross-currency payments is a resource-intensive operation. While that remains true, this fix improves memory usage of pathfinding by discarding trust line results that cannot be used before those results are fully loaded or cached. ([#4111](https://github.com/ripple/rippled/pull/4111)) - -- **Fix incorrect CMake behavior on Windows when platform is unspecified or x64** - Fixes handling of platform selection when using the cmake-gui tool to build on Windows. The generator expects `Win64` but the GUI only provides `x64` as an option, which raises an error. This fix only raises an error if the platform is `Win32` instead, allowing the generation of solution files to succeed. ([#4150](https://github.com/ripple/rippled/pull/4150)) - -- **Fix test failures with newer MSVC compilers on Windows** - Fixes some cases where the API handler code used string pointer comparisons, which may not work correctly with some versions of the MSVC compiler. ([#4149](https://github.com/ripple/rippled/pull/4149)) - -- **Update minimum Boost version to 1.71.0** - This release is compatible with Boost library versions 1.71.0 through 1.77.0. The build configuration and documentation have been updated to reflect this. ([#4134](https://github.com/ripple/rippled/pull/4134)) - -- **Fix unit test failures for DatabaseDownloader** - Increases a timeout in the `DatabaseDownloader` code and adjusts unit tests so that the code does not return spurious failures, and more data is logged if it does fail. ([#4021](https://github.com/ripple/rippled/pull/4021)) - -- **Refactor relational database interface** - Improves code comments, naming, and organization of the module that interfaces with relational databases (such as the SQLite database used for tracking transaction history). ([#3965](https://github.com/ripple/rippled/pull/3965)) - - -## Contributions - -### GitHub - -The public source code repository for `rippled` is hosted on GitHub at . - -We welcome contributions, big and small, and invite everyone to join the community of XRP Ledger developers and help us build the Internet of Value. - - -### Credits - -The following people contributed directly to this release: - -- Devon White -- Ed Hennis -- Gregory Popovitch -- Mark Travis -- Manoj Doshi -- Nik Bougalis -- Richard Holland -- Scott Schurr - -For a real-time view of all lifetime contributors, including links to the commits made by each, please visit the "Contributors" section of the GitHub repository: . - -We welcome external contributions and are excited to see the broader XRP Ledger community continue to grow and thrive. - - -# Change log - -- API version 2 will now return `signer_lists` in the root of the `account_info` response, no longer nested under `account_data`. - -# Releases - -## Version 1.9.0 -This is the 1.9.0 release of `rippled`, the reference implementation of the XRP Ledger protocol. This release brings several features and improvements. - -### New and Improved Features -- **Introduce NFT support (XLS020):** This release introduces support for non-fungible tokens, currently available to the developer community for broader review and testing. Developers can create applications that allow users to mint, transfer, and ultimately burn (if desired) NFTs on the XRP Ledger. You can try out the new NFT transactions using the [nft-devnet](https://xrpl.org/xrp-testnet-faucet.html). Note that some fields and error codes from earlier releases of the supporting code have been refactored for this release, shown in the Code Refactoring section, below. [70779f](https://github.com/ripple/rippled/commit/70779f6850b5f33cdbb9cf4129bc1c259af0013e) - -- **Simplify the Job Queue:** This is a refactor aimed at cleaning up and simplifying the existing job queue. Currently, all jobs are canceled at the same time and in the same way, so this commit removes the unnecessary per-job cancellation token. [#3656](https://github.com/ripple/rippled/pull/3656) - -- **Optimize trust line caching:** The existing trust line caching code was suboptimal in that it stored redundant information, pinned SLEs into memory, and required multiple memory allocations per cached object. This commit eliminates redundant data, reduces the size of cached objects and unpinning SLEs from memory, and uses value types to avoid the need for `std::shared_ptr`. As a result of these changes, the effective size of a cached object includes the overhead of the memory allocator, and the `std::shared_ptr` should be reduced by at least 64 bytes. This is significant, as there can easily be tens of millions of these objects. [4d5459](https://github.com/ripple/rippled/commit/4d5459d041da8f5a349c5f458d664e5865e1f1b5) - -- **Incremental improvements to pathfinding memory usage:** This commit aborts background pathfinding when closed or disconnected, exits the pathfinding job thread if there are no requests left, does not create the path find a job if there are no requests, and refactors to remove the circular dependency between InfoSub and PathRequest. [#4111](https://github.com/ripple/rippled/pull/4111) - -- **Improve deterministic transaction sorting in TxQ:** This commit ensures that transactions with the same fee level are sorted by TxID XORed with the parent ledger hash, the TxQ is re-sorted after every ledger, and attempts to future-proof the TxQ tie-breaking test. [#4077](https://github.com/ripple/rippled/pull/4077) - -- **Improve stop signaling for Application:** [34ca45](https://github.com/ripple/rippled/commit/34ca45713244d0defc39549dd43821784b2a5c1d) - -- **Eliminate SHAMapInnerNode lock contention:** The `SHAMapInnerNode` class had a global mutex to protect the array of node children. Profiling suggested that around 4% of all attempts to lock the global would block. This commit removes that global mutex, and replaces it with a new per-node 16-way spinlock (implemented so as not to affect the size of an inner node object), effectively eliminating the lock contention. [1b9387](https://github.com/ripple/rippled/commit/1b9387eddc1f52165d3243d2ace9be0c62495eea) - -- **Improve ledger-fetching logic:** When fetching ledgers, the existing code would isolate the peer that sent the most useful responses, and issue follow-up queries only to that peer. This commit increases the query aggressiveness, and changes the mechanism used to select which peers to issue follow-up queries to so as to more evenly spread the load among those peers that provided useful responses. [48803a](https://github.com/ripple/rippled/commit/48803a48afc3bede55d71618c2ee38fd9dbfd3b0) - -- **Simplify and improve order book tracking:** The order book tracking code would use `std::shared_ptr` to track the lifetime of objects. This commit changes the logic to eliminate the overhead of `std::shared_ptr` by using value types, resulting in significant memory savings. [b9903b](https://github.com/ripple/rippled/commit/b9903bbcc483a384decf8d2665f559d123baaba2) - -- **Negative cache support for node store:** This commit allows the cache to service requests for nodes that were previously looked up but not found, reducing the need to perform I/O in several common scenarios. [3eb8aa](https://github.com/ripple/rippled/commit/3eb8aa8b80bd818f04c99cee2cfc243192709667) - -- **Improve asynchronous database handlers:** This commit optimizes the way asynchronous node store operations are processed, both by reducing the number of times locks are held and by minimizing the number of memory allocations and data copying. [6faaa9](https://github.com/ripple/rippled/commit/6faaa91850d6b2eb9fbf16c1256bf7ef11ac4646) - -- **Cleanup AcceptedLedger and AcceptedLedgerTx:** This commit modernizes the `AcceptedLedger` and `AcceptedLedgerTx` classes, reduces their memory footprint, and reduces unnecessary dynamic memory allocations. [8f5868](https://github.com/ripple/rippled/commit/8f586870917818133924bf2e11acab5321c2b588) - -### Code Refactoring - -This release includes name changes in the NFToken API for SFields, RPC return labels, and error codes for clarity and consistency. To refactor your code, migrate the names of these items to the new names as listed below. - -#### `SField` name changes: -* `TokenTaxon -> NFTokenTaxon` -* `MintedTokens -> MintedNFTokens` -* `BurnedTokens -> BurnedNFTokens` -* `TokenID -> NFTokenID` -* `TokenOffers -> NFTokenOffers` -* `BrokerFee -> NFTokenBrokerFee` -* `Minter -> NFTokenMinter` -* `NonFungibleToken -> NFToken` -* `NonFungibleTokens -> NFTokens` -* `BuyOffer -> NFTokenBuyOffer` -* `SellOffer -> NFTokenSellOffer` -* `OfferNode -> NFTokenOfferNode` - -#### RPC return labels -* `tokenid -> nft_id` -* `index -> nft_offer_index` - -#### Error codes -* `temBAD_TRANSFER_FEE -> temBAD_NFTOKEN_TRANSFER_FEE` -* `tefTOKEN_IS_NOT_TRANSFERABLE -> tefNFTOKEN_IS_NOT_TRANSFERABLE` -* `tecNO_SUITABLE_PAGE -> tecNO_SUITABLE_NFTOKEN_PAGE` -* `tecBUY_SELL_MISMATCH -> tecNFTOKEN_BUY_SELL_MISMATCH` -* `tecOFFER_TYPE_MISMATCH -> tecNFTOKEN_OFFER_TYPE_MISMATCH` -* `tecCANT_ACCEPT_OWN_OFFER -> tecCANT_ACCEPT_OWN_NFTOKEN_OFFER` - - -### Bug Fixes -- **Fix deletion of orphan node store directories:** Orphaned node store directories should only be deleted if the proper node store directories are confirmed to exist. [06e87e](https://github.com/ripple/rippled/commit/06e87e0f6add5b880d647e14ab3d950decfcf416) - -## Version 1.8.5 -This is the 1.8.5 release of `rippled`, the reference implementation of the XRP Ledger protocol. This release includes fixes and updates for stability and security, and improvements to build scripts. There are no user-facing API or protocol changes in this release. - -### Bug Fixes - -This release contains the following bug fixes and under-the-hood improvements: - -- **Correct TaggedPointer move constructor:** Fixes a bug in unused code for the TaggedPointer class. The old code would fail if a caller explicitly tried to remove a child that is not actually part of the node. (227a12d) - -- **Ensure protocol buffer prerequisites are present:** The build scripts and packages now properly handle Protobuf packages and various packages. Prior to this change, building on Ubuntu 21.10 Impish Indri would fail unless the `libprotoc-dev` package was installed. (e06465f) - -- **Improve handling of endpoints during peer discovery.** This hardens and improves handling of incoming messages on the peer protocol. (289bc0a) - -- **Run tests on updated linux distros:** Test builds now run on Rocky Linux 8, Fedora 34 and 35, Ubuntu 18, 20, and 22, and Debian 9, 10, and 11. (a9ee802) - -- **Avoid dereferencing empty optional in ReportingETL:** Fixes a bug in Reporting Mode that could dereference an empty optional value when throwing an error. (cdc215d) - -- **Correctly add GIT_COMMIT_HASH into version string:** When building the server from a non-tagged release, the build files now add the commit ID in a way that follows the semantic-versioning standard, and correctly handle the case where the commit hash ID cannot be retrieved. (d23d37f) - -- **Update RocksDB to version 6.27.3:** Updates the version of RocksDB included in the server from 6.7.3 (which was released on 2020-03-18) to 6.27.3 (released 2021-12-10). - - - -## Version 1.8.4 -This is the 1.8.4 release of `rippled`, the reference implementation of the XRP Ledger protocol. - -This release corrects a technical flaw introduced with 1.8.3 that may result in failures if the newly-introduced 'fast loading' is enabled. The release also adjusts default parameters used to configure the pathfinding engine to reduce resource usage. - -### Bug Fixes -- **Adjust mutex scope in `walkMapParallel`**: This commit corrects a technical flaw introduced with commit [7c12f0135897361398917ad2c8cda888249d42ae] that would result in undefined behavior if the server operator configured their server to use the 'fast loading' mechanism introduced with 1.8.3. - -- **Adjust pathfinding configuration defaults**: This commit adjusts the default configuration of the pathfinding engine, to account for the size of the XRP Ledger mainnet. Unless explicitly overriden, the changes mean that pathfinding operations will return fewer, shallower paths than previous releases. - - -## Version 1.8.3 -This is the 1.8.3 release of `rippled`, the reference implementation of the XRP Ledger protocol. - -This release implements changes that improve the syncing performance of peers on the network, adds countermeasures to several routines involving LZ4 to defend against CVE-2021-3520, corrects a minor technical flaw that would result in the server not using a cache for nodestore operations, and adjusts tunable values to optimize disk I/O. - -### Summary of Issues -Recently, servers in the XRP Ledger network have been taking an increasingly long time to sync back to the network after restartiningg. This is one of several releases which will be made to improve on this issue. - - -### Bug Fixes - -- **Parallel ledger loader & I/O performance improvements**: This commit makes several changes that, together, should decrease the time needed for a server to sync to the network. To make full use of this change, `rippled` needs to be using storage with high IOPS and operators need to explicitly enable this behavior by adding the following to their config file, under the `[node_db]` stanza: - - [node_db] - ... - fast_load=1 - -Note that when 'fast loading' is enabled the server will not open RPC and WebSocket interfaces until after the initial load is completed. Because of this, it may appear unresponsive or down. - -- **Detect CVE-2021-3520 when decompressing using LZ4**: This commit adds code to detect LZ4 payloads that may result in out-of-bounds memory accesses. - -- **Provide sensible default values for nodestore cache:**: The nodestore includes a built-in cache to reduce the disk I/O load but, by default, this cache was not initialized unless it was explicitly configured by the server operator. This commit introduces sensible defaults based on the server's configured node size. - -- **Adjust the number of concurrent ledger data jobs**: Processing a large amount of data at once can effectively bottleneck a server's I/O subsystem. This commits helps optimize I/O performance by controlling how many jobs can concurrently process ledger data. - -- **Two small SHAMapSync improvements**: This commit makes minor changes to optimize the way memory is used and control the amount of background I/O performed when attempting to fetch missing `SHAMap` nodes. - -## Version 1.8.2 -Ripple has released version 1.8.2 of rippled, the reference server implementation of the XRP Ledger protocol. This release addresses the full transaction queues and elevated transaction fees issue observed on the XRP ledger, and also provides some optimizations and small fixes to improve the server's performance overall. - -### Summary of Issues -Recently, servers in the XRP Ledger network have had full transaction queues and transactions paying low fees have mostly not been able to be confirmed through the queue. After investigation, it was discovered that a large influx of transactions to the network caused it to raise the transaction costs to be proposed in the next ledger block, and defer transactions paying lower costs to later ledgers. The first part worked as designed, but deferred transactions were not being confirmed as the ledger had capacity to process them. - -The root cause was that there were very many low-cost transactions that different servers in the network received in a different order due to incidental differences in timing or network topology, which caused validators to propose different sets of low-cost transactions from the queue. Since none of these transactions had support from a majority of validators, they were removed from the proposed transaction set. Normally, any transactions removed from a proposed transaction set are supposed to be retried in the next ledger, but servers attempted to put these deferred transactions into their transaction queues first, which had filled up. As a result, the deferred transactions were discarded, and the network was only able to confirm transactions that paid high costs. - -### Bug Fixes - -- **Address elevated transaction fees**: This change addresses the full queue problems in two ways. First, it puts deferred transactions directly into the open ledger, rather than transaction queue. This reverts a subset of the changes from [ximinez@62127d7](https://github.com/ximinez/rippled/commit/62127d725d801641bfaa61dee7d88c95e48820c5). A transaction that is in the open ledger but doesn't get validated should stay in the open ledger so that it can be proposed again right away. Second, it changes the order in which transactions are pulled from the transaction queue to increase the overlap in servers' initial transaction consensus proposals. Like the old rules, transactions paying higher fee levels are selected first. Unlike the old rules, transactions paying the same fee level are ordered by transaction ID / hash ascending. (Previously, transactions paying the same fee level were unsorted, resulting in each server having a different order.) - -- **Add ignore_default option to account_lines API**: This flag, if present, suppresses the output of incoming trust lines in the default state. This is primarily motivated by observing that users often have many unwanted incoming trust lines in a default state, which are not useful in the vast majority of cases. Being able to suppress those when doing `account_lines` saves bandwidth and resources. ([#3980](https://github.com/ripple/rippled/pull/3980)) - -- **Make I/O and prefetch worker threads configurable**: This commit adds the ability to specify **io_workers** and **prefetch_workers** in the config file which can be used to specify the number of threads for processing raw inbound and outbound IO and configure the number of threads for performing node store prefetching. ([#3994](https://github.com/ripple/rippled/pull/3994)) - -- **Enforce account RPC limits by objects traversed**: This changes the way the account_objects API method counts and limits the number of objects it returns. Instead of limiting results by the number of objects found, it counts by the number of objects traversed. Additionally, the default and maximum limits for non-admin connections have been decreased. This reduces the amount of work that one API call can do so that public API servers can share load more effectively. ([#4032](https://github.com/ripple/rippled/pull/4032)) - -- **Fix a crash on shutdown**: The NuDB backend class could throw an error in its destructor, resulting in a crash while the server was shutting down gracefully. This crash was harmless but resulted in false alarms and noise when tracking down other possible crashes. ([#4017](https://github.com/ripple/rippled/pull/4017)) - -- **Improve reporting of job queue in admin server_info**: The server_info command, when run with admin permissions, provides information about jobs in the server's job queue. This commit provides more descriptive names and more granular categories for many jobs that were previously all identified as "clientCommand". ([#4031](https://github.com/ripple/rippled/pull/4031)) - -- **Improve full & compressed inner node deserialization**: Remove a redundant copy operation from low-level SHAMap deserialization. ([#4004](https://github.com/ripple/rippled/pull/4004)) - -- **Reporting mode: only forward to P2P nodes that are synced**: Previously, reporting mode servers forwarded to any of their configured P2P nodes at random. This commit improves the selection so that it only chooses from P2P nodes that are fully synced with the network. ([#4028](https://github.com/ripple/rippled/pull/4028)) - -- **Improve handling of HTTP X-Forwarded-For and Forwarded headers**: Fixes the way the server handles IPv6 addresses in these HTTP headers. ([#4009](https://github.com/ripple/rippled/pull/4009), [#4030](https://github.com/ripple/rippled/pull/4030)) - -- **Other minor improvements to logging and Reporting Mode.** - - -## Version 1.8.0 -Ripple has released version 1.8.0 of rippled, the reference server implementation of the XRP Ledger protocol. This release brings several features and improvements. - -### New and Improved Features - -- **Improve History Sharding**: Shards of ledger history are now assembled in a deterministic way so that any server can make a binary-identical shard for a given range of ledgers. This makes it possible to retrieve a shard from multiple sources in parallel, then verify its integrity by comparing checksums with peers' checksums for the same shard. Additionally, there's a new admin RPC command to import ledger history from the shard store, and the crawl_shards command has been expanded with more info. ([#2688](https://github.com/ripple/rippled/issues/2688), [#3726](https://github.com/ripple/rippled/pull/3726), [#3875](https://github.com/ripple/rippled/pull/3875)) -- **New CheckCashMakesTrustLine Amendment**: If enabled, this amendment will change the CheckCash transaction type so that cashing a check for an issued token automatically creates a trust line to hold the token, similar to how purchasing a token in the decentralized exchange creates a trust line to hold the token. This change provides a way for issuers to send tokens to a user before that user has set up a trust line, but without forcing anyone to hold tokens they don't want. ([#3823](https://github.com/ripple/rippled/pull/3823)) -- **Automatically determine the node size**: The server now selects an appropriate `[node_size]` configuration value by default if it is not explicitly specified. This parameter tunes various settings to the specs of the hardware that the server is running on, especially the amount of RAM and the number of CPU threads available in the system. Previously the server always chose the smallest value by default. -- **Improve transaction relaying logic**: Previously, the server relayed every transaction to all its peers (except the one that it received the transaction from). To reduce redundant messages, the server now relays transactions to a subset of peers using a randomized algorithm. Peers can determine whether there are transactions they have not seen and can request them from a peer that has them. It is expected that this feature will further reduce the bandwidth needed to operate a server. -- **Improve the Byzantine validator detector**: This expands the detection capabilities of the Byzantine validation detector. Previously, the server only monitored validators on its own UNL. Now, the server monitors for Byzantine behavior in all validations it sees. -- **Experimental tx stream with history for sidechains**: Adds an experimental subscription stream for sidechain federators to track messages on the main chain in canonical order. This stream is expected to change or be replaced in future versions as work on sidechains matures. -- **Support Debian 11 Bullseye**: This is the first release that is compatible with Debian Linux version 11.x, "Bullseye." The .deb packages now use absolute paths only, for compatibility with Bullseye's stricter package requirements. ([#3909](https://github.com/ripple/rippled/pull/3909)) -- **Improve Cache Performance**: The server uses a new storage structure for several in-memory caches for greatly improved overall performance. The process of purging old data from these caches, called "sweeping", was time-consuming and blocked other important activities necessary for maintaining ledger state and participating in consensus. The new structure divides the caches into smaller partitions that can be swept in parallel. -- **Amendment default votes:** Introduces variable default votes per amendment. Previously the server always voted "yes" on any new amendment unless an admin explicitly configured a voting preference for that amendment. Now the server's default vote can be "yes" or "no" in the source code. This should allow a safer, more gradual roll-out of new amendments, as new releases can be configured to understand a new amendment but not vote for it by default. ([#3877](https://github.com/ripple/rippled/pull/3877)) -- **More fields in the `validations` stream:** The `validations` subscription stream in the API now reports additional fields that were added to validation messages by the HardenedValidations amendment. These fields make it easier to detect misconfigurations such as multiple servers sharing a validation key pair. ([#3865](https://github.com/ripple/rippled/pull/3865)) -- **Reporting mode supports `validations` and `manifests` streams:** In the API it is now possible to connect to these streams when connected to a servers running in reporting. Previously, attempting to subscribe to these streams on a reporting server failed with the error `reportingUnsupported`. ([#3905](https://github.com/ripple/rippled/pull/3905)) - -### Bug Fixes - -- **Clarify the safety of NetClock::time_point arithmetic**: * NetClock::rep is uint32_t and can be error-prone when used with subtraction. * Fixes [#3656](https://github.com/ripple/rippled/pull/3656) -- **Fix out-of-bounds reserve, and some minor optimizations** -- **Fix nested locks in ValidatorSite** -- **Fix clang warnings about copies vs references** -- **Fix reporting mode build issue** -- **Fix potential deadlock in Validator sites** -- **Use libsecp256k1 instead of OpenSSL for key derivation**: The deterministic key derivation code was still using calls to OpenSSL. This replaces the OpenSSL-based routines with new libsecp256k1-based implementations -- **Improve NodeStore to ShardStore imports**: This runs the import process in a background thread while preventing online_delete from removing ledgers pending import -- **Simplify SHAMapItem construction**: The existing class offered several constructors which were mostly unnecessary. This eliminates all existing constructors and introduces a single new one, taking a `Slice`. The internal buffer is switched from `std::vector` to `Buffer` to save a minimum of 8 bytes (plus the buffer slack that is inherent in `std::vector`) per SHAMapItem instance. -- **Redesign stoppable objects**: Stoppable is no longer an abstract base class, but a pattern, modeled after the well-understood `std::thread`. The immediate benefits are less code, less synchronization, less runtime work, and (subjectively) more readable code. The end goal is to adhere to RAII in our object design, and this is one necessary step on that path. - -## Version 1.7.3 - -This is the 1.7.3 release of `rippled`, the reference implementation of the XRP Ledger protocol. This release addresses an OOB memory read identified by Guido Vranken, as well as an unrelated issue identified by the Ripple C++ team that could result in incorrect use of SLEs. Additionally, this version also introduces the `NegativeUNL` amendment, which corresponds to the feature which was introduced with the 1.6.0 release. - -## Action Required - -If you operate an XRP Ledger server, then you should upgrade to version 1.7.3 at your earliest convenience to mitigate the issues addressed in this hotfix. If a sufficient majority of servers on the network upgrade, the `NegativeUNL` amendment may gain a majority, at which point a two week activation countdown will begin. If the `NegativeUNL` amendment activates, servers running versions of `rippled` prior to 1.7.3 will become [amendment blocked](https://xrpl.org/amendments.html#amendment-blocked). - -### Bug Fixes - -- **Improve SLE usage in check cashing**: Fixes a situation which could result in the incorrect use of SLEs. -- **Address OOB in base58 decoder**: Corrects a technical flaw that could allow an out-of-bounds memory read in the Base58 decoder. -- **Add `NegativeUNL` as a supported amendment**: Introduces an amendment for the Negative UNL feature introduced in `rippled` 1.6.0. - -## Version 1.7.2 - -This the 1.7.2 release of rippled, the reference server implementation of the XRP Ledger protocol. This release protects against the security issue [CVE-2021-3499](https://www.openssl.org/news/secadv/20210325.txt) affecting OpenSSL, adds an amendment to fix an issue with small offers not being properly removed from order books in some cases, and includes various other minor fixes. -Version 1.7.2 supersedes version 1.7.1 and adds fixes for more issues that were discovered during the release cycle. - -## Action Required - -This release introduces a new amendment to the XRP Ledger protocol: `fixRmSmallIncreasedQOffers`. This amendments is now open for voting according to the XRP Ledger's amendment process, which enables protocol changes following two weeks of >80% support from trusted validators. -If you operate an XRP Ledger server, then you should upgrade to version 1.7.2 within two weeks, to ensure service continuity. The exact time that protocol changes take effect depends on the voting decisions of the decentralized network. -If you operate an XRP Ledger validator, please learn more about this amendment so you can make informed decisions about how your validator votes. If you take no action, your validator begins voting in favor of any new amendments as soon as it has been upgraded. - -### Bug Fixes - -- **fixRmSmallIncreasedQOffers Amendment:** This amendment fixes an issue where certain small offers can be left at the tip of an order book without being consumed or removed when appropriate and causes some payments and Offers to fail when they should have succeeded [(#3827)](https://github.com/ripple/rippled/pull/3827). -- **Adjust OpenSSL defaults and mitigate CVE-2021-3499:** Prior to this fix, servers compiled against a vulnerable version of OpenSSL could have a crash triggered by a malicious network connection. This fix disables renegotiation support in OpenSSL so that the rippled server is not vulnerable to this bug regardless of the OpenSSL version used to compile the server. This also removes support for deprecated TLS versions 1.0 and 1.1 and ciphers that are not part of TLS 1.2 [(#79e69da)](https://github.com/ripple/rippled/pull/3843/commits/79e69da3647019840dca49622621c3d88bc3883f). -- **Support HTTP health check in reporting mode:** Enables the Health Check special method when running the server in the new Reporting Mode introduced in 1.7.0 [(9c8cadd)](https://github.com/ripple/rippled/pull/3843/commits/9c8caddc5a197bdd642556f8beb14f06d53cdfd3). -- **Maintain compatibility for forwarded RPC responses:** Fixes a case in API responses from servers in Reporting Mode, where requests that were forwarded to a P2P-mode server would have the result field nested inside another result field [(8579eb0)](https://github.com/ripple/rippled/pull/3843/commits/8579eb0c191005022dcb20641444ab471e277f67). -- **Add load_factor in reporting mode:** Adds a load_factor value to the server info method response when running the server in Reporting Mode so that the response is compatible with the format returned by servers in P2P mode (the default) [(430802c)](https://github.com/ripple/rippled/pull/3843/commits/430802c1cf6d4179f2249a30bfab9eff8e1fa748). -- **Properly encode metadata from tx RPC command:** Fixes a problem where transaction metadata in the tx API method response would be in JSON format even when binary was requested [(7311629)](https://github.com/ripple/rippled/pull/3843/commits/73116297aa94c4acbfc74c2593d1aa2323b4cc52). -- **Updates to Windows builds:** When building on Windows, use vcpkg 2021 by default and add compatibility with MSVC 2019 [(36fe196)](https://github.com/ripple/rippled/pull/3843/commits/36fe1966c3cd37f668693b5d9910fab59c3f8b1f), [(30fd458)](https://github.com/ripple/rippled/pull/3843/commits/30fd45890b1d3d5f372a2091d1397b1e8e29d2ca). - -## Version 1.7.0 - -Ripple has released version 1.7.0 of `rippled`, the reference server implementation of the XRP Ledger protocol. -This release [significantly improves memory usage](https://blog.ripplex.io/how-ripples-c-team-cut-rippleds-memory-footprint-down-to-size/), introduces a protocol amendment to allow out-of-order transaction execution with Tickets, and brings several other features and improvements. - -## Upgrading (SPECIAL ACTION REQUIRED) -If you use the precompiled binaries of rippled that Ripple publishes for supported platforms, please note that Ripple has renewed the GPG key used to sign these packages. -If you are upgrading from a previous install, you must download and trust the renewed key. Automatic upgrades will not work until you have re-trusted the key. -### Red Hat Enterprise Linux / CentOS - -Perform a [manual upgrade](https://xrpl.org/update-rippled-manually-on-centos-rhel.html). When prompted, confirm that the key's fingerprint matches the following example, then press `y` to accept the updated key: - -``` -$ sudo yum install rippled -Loaded plugins: fastestmirror -Loading mirror speeds from cached hostfile -* base: mirror.web-ster.com -* epel: mirrors.syringanetworks.net -* extras: ftp.osuosl.org -* updates: mirrors.vcea.wsu.edu -ripple-nightly/signature | 650 B 00:00:00 -Retrieving key from https://repos.ripple.com/repos/rippled-rpm/nightly/repodata/repomd.xml.key -Importing GPG key 0xCCAFD9A2: -Userid : "TechOps Team at Ripple " -Fingerprint: c001 0ec2 05b3 5a33 10dc 90de 395f 97ff ccaf d9a2 -From : https://repos.ripple.com/repos/rippled-rpm/nightly/repodata/repomd.xml.key -Is this ok [y/N]: y -``` - -### Ubuntu / Debian - -Download and trust the updated public key, then perform a [manual upgrade](https://xrpl.org/update-rippled-manually-on-ubuntu.html) as follows: - -``` -wget -q -O - "https://repos.ripple.com/repos/api/gpg/key/public" | \ - sudo apt-key add - -sudo apt -y update -sudo apt -y install rippled -``` - -### New and Improved Features - -- **Rework deferred node logic and async fetch behavior:** This change significantly improves ledger sync and fetch times while reducing memory consumption. (https://blog.ripplex.io/how-ripples-c-team-cut-rippleds-memory-footprint-down-to-size/) -- **New Ticket feature:** Tickets are a mechanism to prepare and send certain transactions outside of the normal sequence order. This version reworks and completes the implementation for Tickets after more than 6 years of development. This feature is now open for voting as the newly-introduced `TicketBatch` amendment, which replaces the previously-proposed `Tickets` amendment. The specification for this change can be found at: [xrp-community/standards-drafts#16](https://github.com/xrp-community/standards-drafts/issues/16) -- **Add Reporting Mode:** The server can be compiled to operate in a new mode that serves API requests for validated ledger data without connecting directly to the peer-to-peer network. (The server needs a gRPC connection to another server that is on the peer-to-peer network.) Reporting Mode servers can share access to ledger data via Apache Cassandra and PostgreSQL to more efficiently serve API requests while peer-to-peer servers specialize in broadcasting and processing transactions. -- **Optimize relaying of validation and proposal messages:** Servers typically receive multiple copies of any given message from directly connected peers; in particular, consensus proposal and validation messages are often relayed with extremely high redundancy. For servers with several peers, this can cause redundant work. This commit introduces experimental code that attempts to optimize the relaying of proposals and validations by allowing servers to instruct their peers to "squelch" delivery of selected proposals and validations. This change is considered experimental at this time and is disabled by default because the functioning of the consensus network depends on messages propagating with high reliability through the constantly-changing peer-to-peer network. Server operators who wish to test the optimized code can enable it in their server config file. -- **Report server domain to other servers:** Server operators now have the option to configure a domain name to be associated with their servers. The value is communicated to other servers and is also reported via the `server_info` API. The value is meant for third-party applications and tools to group servers together. For example, a tool that visualizes the network's topology can show how many servers are operated by different stakeholders. An operator can claim any domain, so tools should use the [xrp-ledger.toml file](https://xrpl.org/xrp-ledger-toml.html) to confirm that the domain also claims ownership of the servers. -- **Improve handling of peers that aren't synced:** When evaluating the fitness and usefulness of an outbound peer, the code would incorrectly calculate the amount of time that the peer spent in a non-useful state. This release fixes the calculation and makes the timeout values configurable by server operators. Two new options are introduced in the 'overlay' stanza of the config file. -- **Persist API-configured voting settings:** Previously, the amendments that a server would vote in support of or against could be configured both via the configuration file and via the ["feature" API method](https://xrpl.org/feature.html). Changes made in the configuration file were only loaded at server startup; changes made via the command line take effect immediately but were not persisted across restarts. Starting with this release, changes made via the API are saved to the wallet.db database file so that they persist even if the server is restarted. -Amendment voting in the config file is deprecated. The first time the server starts with v1.7.0 or higher, it reads any amendment voting settings in the config file and saves the settings to the database; on later restarts the server prints a warning message and ignores the [amendments] and [veto_amendments] stanzas of the config file. -Going forward, use the [feature method](https://xrpl.org/feature.html) to view and configure amendment votes. If you want to use the config file to configure amendment votes, add a line to the [rpc_startup] stanza such as the following: -[rpc_startup] -{ "command": "feature", "feature": "FlowSortStrands", "vetoed": true } -- **Support UNLs with future effective dates:** Updates the format for the recommended validator list file format, allowing publishers to pre-publish the next recommended UNL while the current one is still valid. The server is still backwards compatible with the previous format, but the new format removes some uncertainty during the transition from one list to the next. Also, starting with this release, the server locks down and reports an error if it has no valid validator list. You can clear the error by loading a validator list from a file or by configuring a different UNL and restarting; the error also goes away on its own if the server is able to obtain a trusted validator list from the network (for example, after an network outage resolves itself). -- **Improve manifest relaying:** Servers now propagate change messages for validators' ephemeral public keys ("manifests") on a best-effort basis, to make manifests more available throughout the peer-to-peer network. Previously, the server would only relay manifests from validators it trusts locally, which made it difficult to detect and track validators that are not broadly trusted. -- **Implement ledger forward replay feature:** The server can now sync up to the network by "playing forward" transactions from a previously saved ledger until it catches up to the network. Compared with the default behavior of fetching the latest state and working backwards, forward replay can save time and bandwidth by reconstructing previous ledgers' state data rather than downloading the pre-calculated results from the network. As an added bonus, forward replay confirms that the rest of the network followed the same transaction processing rules as the local server when processing the intervening ledgers. This feature is considered experimental this time and can be enabled with an option in the config file. -- **Make the transaction job queue limit adjustable:** The server uses a job queue to manage tasks, with limits on how many jobs of a particular type can be queued. The previously hard-coded limit associated with transactions is now configurable. Server operators can increase the number of transactions their server is able to queue, which may be useful if your server has a large memory capacity or you expect an influx of transactions. (https://github.com/ripple/rippled/issues/3556) -- **Add public_key to the Validator List method response:** The [Validator List method](https://xrpl.org/validator-list.html) can be used to request a recommended validator list from a rippled instance. The response now includes the public key of the requested list. (https://github.com/ripple/rippled/issues/3392) -- **Server operators can now configure maximum inbound and outbound peers separately:** The new `peers_in_max` and `peers_out_max` config options allow server operators to independently control the maximum number of inbound and outbound peers the server allows. [70c4ecc] -- **Improvements to shard downloading:** Previously the download_shard command could only load shards over HTTPS. Compressed shards can now also be downloaded over plain HTTP. The server fully checks the data for integrity and consistency, so the encryption is not strictly necessary. When initiating multiple shard downloads, the server now returns an error if there is not enough space to store all the shards currently being downloaded. -- **The manifest command is now public:** The manifest API method returns public information about a given validator. The required permissions have been changed so it is now part of the public API. - -### Bug Fixes - -- **Implement sticky DNS resolution for validator list retrieval:** When attempting to load a validator list from a configured site, attempt to reuse the last IP that was successfully used if that IP is still present in the DNS response. (https://github.com/ripple/rippled/issues/3494). -- **Improve handling of RPC ledger_index argument:** You can now provide the `ledger_index` as a numeric string. This allows you to copy and use the numeric string `ledger_index` value returned by certain RPC commands. Previously you could only send native JSON numbers or shortcut strings such as "validated" in the `ledger_index` field. (https://github.com/ripple/rippled/issues/3533) -- **Fix improper promotion of bool on return** [6968da1] -- **Fix ledger sequence on copynode** [ef53197] -- **Fix parsing of node public keys in `manifest` CLI:** The previous code attempts to validate the provided node public key using a function that assumes that the encoded public key is for an account. This causes the parsing to fail. This commit fixes #3317 (https://github.com/ripple/rippled/issues/3317) by letting the caller specify the type of the public key being checked. -- **Fix idle peer timer:** Fixes a bug where a function to remove idle peers was called every second instead of every 4 seconds. #3754 (https://github.com/ripple/rippled/issues/3754) -- **Add database counters:** Fix bug where DatabaseRotateImp::getBackend and ::sync utilized the writable backend without a lock. ::getBackend was replaced with ::getCounters. -- **Improve online_delete configuration and DB tuning** [6e9051e] -- **Improve handling of burst writes in NuDB database** ( https://github.com/ripple/rippled/pull/3662 ) -- **Fix excessive logging after disabling history shards.** Previously if you configured the server with a shard store, then disabled it, the server output excessive warning messages about the shard limit being exceeded. -- **Fixed some issues with negotiating link compression.** ( https://github.com/ripple/rippled/pull/3705 ) -- **Fixed a potential thread deadlock with history sharding.** ( https://github.com/ripple/rippled/pull/3683 ) -- **Various fixes to typos and comments, refactoring, and build system improvements** - -## Version 1.6.0 - -This release introduces several new features including changes to the XRP Ledger's consensus mechanism to make it even more robust in -adverse conditions, as well as numerous bug fixes and optimizations. - -### New and Improved Features - -- Initial implementation of Negative UNL functionality: This change can improve the liveness of the network during periods of network instability, by allowing servers to track which validators are temporarily offline and to adjust quorum calculations to match. This change requires an amendment, but the amendment is not in the **1.6.0** release. Ripple expects to run extensive public testing for Negative UNL functionality on the Devnet in the coming weeks. If public testing satisfies all requirements across security, reliability, stability, and performance, then the amendment could be included in a version 2.0 release. [[#3380](https://github.com/ripple/rippled/pull/3380)] -- Validation Hardening: This change allows servers to detect accidental misconfiguration of validators, as well as potentially Byzantine behavior by malicious validators. Servers can now log a message to notify operators if they detect a single validator issuing validations for multiple, incompatible ledger versions, or validations from multiple servers sharing a key. As part of this update, validators report the version of `rippled` they are using, as well as the hash of the last ledger they consider to be fully validated, in validation messages. [[#3291](https://github.com/ripple/rippled/pull/3291)] ![Amendment: Required](https://img.shields.io/badge/Amendment-Required-red) -- Software Upgrade Monitoring & Notification: After the `HardenedValidations` amendment is enabled and the validators begin reporting the versions of `rippled` they are running, a server can check how many of the validators on its UNL run a newer version of the software than itself. If more than 60% of a server's validators are running a newer version, the server writes a message to notify the operator to consider upgrading their software. [[#3447](https://github.com/ripple/rippled/pull/3447)] -- Link Compression: Beginning with **1.6.0**, server operators can enable support for compressing peer-to-peer messages. This can save bandwidth at a cost of higher CPU usage. This support is disabled by default and should prove useful for servers with a large number of peers. [[#3287](https://github.com/ripple/rippled/pull/3287)] -- Unconditionalize Amendments that were enabled in 2017: This change removes legacy code which the network has not used since 2017. This change limits the ability to [replay](https://github.com/xrp-community/standards-drafts/issues/14) ledgers that rely on the pre-2017 behavior. [[#3292](https://github.com/ripple/rippled/pull/3292)] -- New Health Check Method: Perform a simple HTTP request to get a summary of the health of the server: Healthy, Warning, or Critical. [[#3365](https://github.com/ripple/rippled/pull/3365)] -- Start work on API version 2. Version 2 of the API will be part of a future release. The first breaking change will be to consolidate several closely related error messages that can occur when the server is not synced into a single "notSynced" error message. [[#3269](https://github.com/ripple/rippled/pull/3269)] -- Improved shard concurrency: Improvements to the shard engine have helped reduce the lock scope on all public functions, increasing the concurrency of the code. [[#3251](https://github.com/ripple/rippled/pull/3251)] -- Default Port: In the config file, the `[ips_fixed]` and `[ips]` stanzas now use the [IANA-assigned port](https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=2459) for the XRP Ledger protocol (2459) when no port is specified. The `connect` API method also uses the same port by default. [[#2861](https://github.com/ripple/rippled/pull/2861)]. -- Improve proposal and validation relaying. The peer-to-peer protocol always relays trusted proposals and validations (as part of the [consensus process](https://xrpl.org/consensus.html)), but only relays _untrusted_ proposals and validations in certain circumstances. This update adds configuration options so server operators can fine-tune how their server handles untrusted proposals and validations, and changes the default behavior to prioritize untrusted validations higher than untrusted proposals. [[#3391](https://github.com/ripple/rippled/pull/3391)] -- Various Build and CI Improvements including updates to RocksDB 6.7.3 [[#3356](https://github.com/ripple/rippled/pull/3356)], NuDB 2.0.3 [[#3437](https://github.com/ripple/rippled/pull/3437)], adjusting CMake settings so that rippled can be built as a submodule [[#3449](https://github.com/ripple/rippled/pull/3449)], and adding Travis CI settings for Ubuntu Bionic Beaver [[#3319](https://github.com/ripple/rippled/pull/3319)]. -- Better documentation in the config file for online deletion and database tuning. [[#3429](https://github.com/ripple/rippled/pull/3429)] - - -### Bug Fixes - -- Fix the 14 day timer to enable amendment to start at the correct quorum size [[#3396](https://github.com/ripple/rippled/pull/3396)] -- Improve online delete backend lock which addresses a possibility in the online delete process where one or more backend shared pointer references may become invalid during rotation. [[#3342](https://github.com/ripple/rippled/pull/3342)] -- Address an issue that can occur during the loading of validator tokens, where a deliberately malformed token could cause the server to crash during startup. [[#3326](https://github.com/ripple/rippled/pull/3326)] -- Add delivered amount to GetAccountTransactionHistory. The delivered_amount field was not being populated when calling GetAccountTransactionHistory. In contrast, the delivered_amount field was being populated when calling GetTransaction. This change populates delivered_amount in the response to GetAccountTransactionHistory, and adds a unit test to make sure the results delivered by GetTransaction and GetAccountTransactionHistory match each other. [[#3370](https://github.com/ripple/rippled/pull/3370)] -- Fix build issues for GCC 10 [[#3393](https://github.com/ripple/rippled/pull/3393)] -- Fix historical ledger acquisition - this fixes an issue where historical ledgers were acquired only since the last online deletion interval instead of the configured value to allow deletion.[[#3369](https://github.com/ripple/rippled/pull/3369)] -- Fix build issue with Docker [#3416](https://github.com/ripple/rippled/pull/3416)] -- Add Shard family. The App Family utilizes a single shared Tree Node and Full Below cache for all history shards. This can create a problem when acquiring a shard that shares an account state node that was recently cached from another shard operation. The new Shard Family class solves this issue by managing separate Tree Node and Full Below caches for each shard. [#3448](https://github.com/ripple/rippled/pull/3448)] -- Amendment table clean up which fixes a calculation issue with majority. [#3428](https://github.com/ripple/rippled/pull/3428)] -- Add the `ledger_cleaner` command to rippled command line help [[#3305](https://github.com/ripple/rippled/pull/3305)] -- Various typo and comments fixes. - - -## Version 1.5.0 - -The `rippled` 1.5.0 release introduces several improvements and new features, including support for gRPC API, API versioning, UNL propagation via the peer network, new RPC methods `validator_info` and `manifest`, augmented `submit` method, improved `tx` method response, improved command line parsing, improved handshake protocol, improved package building and various other minor bug fixes and improvements. - -This release also introduces two new amendments: `fixQualityUpperBound` and `RequireFullyCanonicalSig`. - -Several improvements to the sharding system are currently being evaluated for inclusion into the upcoming 1.6 release of `rippled`. These changes are incompatible with shards generated by previous versions of the code. -Additionally, an issue with the existing sharding engine can result in a server running versions 1.4 or 1.5 of the software to experience a deadlock and automatically restart when running with the sharding feature enabled. -At this time, the developers recommend running with sharding disabled, pending the improvements scheduled to be introduced with 1.6. For more information on how to disable sharding, please visit https://xrpl.org/configure-history-sharding.html - - -**New and Updated Features** -- The `RequireFullyCanonicalSig` amendment which changes the signature requirements for the XRP Ledger protocol so that non-fully-canonical signatures are no longer valid. This protects against transaction malleability on all transactions, instead of just transactions with the tfFullyCanonicalSig flag enabled. Without this amendment, a transaction is malleable if it uses a secp256k1 signature and does not have tfFullyCanonicalSig enabled. Most signing utilities enable tfFullyCanonicalSig by default, but there are exceptions. With this amendment, no single-signed transactions are malleable. (Multi-signed transactions may still be malleable if signers provide more signatures than are necessary.) All transactions must use the fully canonical form of the signature, regardless of the tfFullyCanonicalSig flag. Signing utilities that do not create fully canonical signatures are not supported. All of Ripple's signing utilities have been providing fully-canonical signatures exclusively since at least 2014. For more information. [`ec137044a`](https://github.com/ripple/rippled/commit/ec137044a014530263cd3309d81643a5a3c1fdab) -- Native [gRPC API](https://grpc.io/) support. Currently, this API provides a subset of the full `rippled` [API](https://xrpl.org/rippled-api.html). You can enable the gRPC API on your server with a new configuration stanza. [`7d867b806`](https://github.com/ripple/rippled/commit/7d867b806d70fc41fb45e3e61b719397033b272c) -- API Versioning which allows for future breaking change of RPC methods to co-exist with existing versions. [`2aa11fa41`](https://github.com/ripple/rippled/commit/2aa11fa41d4a7849ae6a5d7a11df6f367191e3ef) -- Nodes now receive and broadcast UNLs over the peer network under various conditions. [`2c71802e3`](https://github.com/ripple/rippled/commit/2c71802e389a59118024ea0152123144c084b31c) -- Augmented `submit` method to include additional details on the status of the command. [`79e9085dd`](https://github.com/ripple/rippled/commit/79e9085dd1eb72864afe841225b78ec96e72b5ca) -- Improved `tx` method response with additional details on ledgers searched. [`47501b7f9`](https://github.com/ripple/rippled/commit/47501b7f99d4103d9ad405e399169fc251161548) -- New `validator_info` method which returns information pertaining to the current validator's keys, manifest sequence, and domain. [`3578acaf0`](https://github.com/ripple/rippled/commit/3578acaf0b5f2d27ddc33f5b4cc81d21be1903ae) -- New `manifest` method which looks up manifest information for the specified key (either master or ephemeral). [`3578acaf0`](https://github.com/ripple/rippled/commit/3578acaf0b5f2d27ddc33f5b4cc81d21be1903ae) -- Introduce handshake protocol for compression negotiation (compression is not implemented at this point) and other minor improvements. [`f6916bfd4`](https://github.com/ripple/rippled/commit/f6916bfd429ce654e017ae9686cb023d9e05408b) -- Remove various old conditionals introduced by amendments. [`(51ed7db00`](https://github.com/ripple/rippled/commit/51ed7db0027ba822739bd9de6f2613f97c1b227b), [`6e4945c56)`](https://github.com/ripple/rippled/commit/6e4945c56b1a1c063b32921d7750607587ec3063) -- Add `getRippledInfo` info gathering script to `rippled` Linux packages. [`acf4b7889`](https://github.com/ripple/rippled/commit/acf4b78892074303cb1fa22b778da5e7e7eddeda) - -**Bug Fixes and Improvements** -- The `fixQualityUpperBound` amendment which fixes a bug in unused code for estimating the ratio of input to output of individual steps in cross-currency payments. [`9d3626fec`](https://github.com/ripple/rippled/commit/9d3626fec5b610100f401dc0d25b9ec8e4a9a362) -- `tx` method now properly fetches all historical tx if they are incorporated into a validated ledger under rules that applied at the time. [`11cf27e00`](https://github.com/ripple/rippled/commit/11cf27e00698dbfc099b23463927d1dac829ed19) -- Fix to how `fail_hard` flag is handled with the `submit` method - transactions that are submitted with the `fail_hard` flag that result in any TER code besides tesSUCCESS is neither queued nor held. [`cd9732b47`](https://github.com/ripple/rippled/commit/cd9732b47a9d4e95bcb74e048d2c76fa118b80fb) -- Remove unused `Beast` code. [`172ead822`](https://github.com/ripple/rippled/commit/172ead822159a3c1f9b73217da4316df48851ab6) -- Lag ratchet code fix to use proper ephemeral public keys instead of the long-term master public keys.[`6529d3e6f`](https://github.com/ripple/rippled/commit/6529d3e6f7333fc5226e5aa9ae65f834cb93dfe5) - - -## Version 1.4.0 - -The `rippled` 1.4.0 release introduces several improvements and new features, including support for deleting accounts, improved peer slot management, improved CI integration and package building and support for [C++17](https://en.wikipedia.org/wiki/C%2B%2B17) and [Boost 1.71](https://www.boost.org/users/history/version_1_71_0.html). Finally, this release removes the code for the `SHAMapV2` amendment which failed to gain majority support and has been obsoleted by other improvements. - -**New and Updated Features** -- The `DeletableAccounts` amendment which, if enabled, will make it possible for users to delete unused or unneeded accounts, recovering the account's reserve. -- Support for improved management of peer slots and the ability to add and removed reserved connections without requiring a restart of the server. -- Tracking and reporting of cumulative and instantaneous peer bandwidth usage. -- Preliminary support for post-processing historical shards after downloading to index their contents. -- Reporting the master public key alongside the ephemeral public key in the `validation` stream [subscriptions](https://xrpl.org/subscribe.html). -- Reporting consensus phase changes in the `server` stream [subscription](https://xrpl.org/subscribe.html). - -**Bug Fixes** -- The `fixPayChanRecipientOwnerDir` amendment which corrects a minor technical flaw that would cause a payment channel to not appear in the recipient's owner directory, which made it unnecessarily difficult for users to enumerate all their payment channels. -- The `fixCheckThreading` amendment which corrects a minor technical flaw that caused checks to not be properly threaded against the account of the check's recipient. -- Respect the `ssl_verify` configuration option in the `SSLHTTPDownloader` and `HTTPClient` classes. -- Properly update the `server_state` when a server detects a disagreement between itself and the network. -- Allow Ed25519 keys to be used with the `channel_authorize` command. - -## Version 1.3.1 - -The `rippled` 1.3.1 release improves the built-in deadlock detection code, improves logging during process startup, changes the package build pipeline and improves the build documentation. - -**New and Updated Features** - -This release has no new features. - -**Bug Fixes** -- Add a LogicError when a deadlock is detected (355a7b04) -- Improve logging during process startup (7c24f7b1) - -## Version 1.3.0 -The `rippled` 1.3.0 release introduces several new features and overall improvements to the codebase, including the `fixMasterKeyAsRegularKey` amendment, code to adjust the timing of the consensus process and support for decentralized validator domain verification. The release also includes miscellaneous improvements including in the transaction censorship detection code, transaction validation code, manifest parsing code, configuration file parsing code, log file rotation code, and in the build, continuous integration, testing and package building pipelines. - -**New and Updated Features** -- The `fixMasterKeyAsRegularKey` amendment which, if enabled, will correct a technical flaw that allowed setting an account's regular key to the account's master key. -- Code that allows validators to adjust the timing of the consensus process in near-real-time to account for connection delays. -- Support for decentralized validator domain verification by adding support for a "domain" field in manifests. - -**Bug Fixes** -- Improve ledger trie ancestry tracking to reduce unnecessary error messages. -- More efficient detection of dry paths in the payment engine. Although not a transaction-breaking change, this should reduce spurious error messages in the log files. - -## Version 1.2.4 - -The `rippled` 1.2.4 release improves the way that shard crawl requests are routed and the robustness of configured validator list retrieval by imposing a 20 second timeout. - -**New and Updated Features** - -This release has no new features. - -**Bug Fixes** - -- Use public keys when routing shard crawl requests -- Enforce a 20s timeout when making validator list requests (RIPD-1737) - -## Version 1.2.3 - -The `rippled` 1.2.3 release corrects a technical flaw which in some circumstances can cause a null pointer dereference that can crash the server. - -**New and Updated Features** - -This release has no new features. - -**Bug Fixes** - -- Fix a technical flaw which in some circumstances can cause a null pointer dereference that can crash the server. - -## Version 1.2.2 - -The `rippled` 1.2.2 release corrects a technical flaw in the fee escalation -engine which could cause some fee metrics to be calculated incorrectly. In some -circumstances this can potentially cause the server to crash. - -**New and Updated Features** - -This release has no new features. - -**Bug Fixes** - -- Fix a technical flaw in the fee escalation engine which could cause some fee metrics to be calculated incorrectly (4c06b3f86) - -## Version 1.2.1 - -The `rippled` 1.2.1 release introduces several fixes including a change in the -information reported via the enhanced crawl functionality introduced in the -1.2.0 release, a fix for a potential race condition when processing a status -change message for a peer, and for a technical flaw that could cause a server -to not properly detect that it had lost all its peers. - -The release also adds the `delivered_amount` field to more responses to simplify -the handling of payment or check cashing transactions. - -**New and Updated Features** - -This release has no new features. - -**Bug Fixes** - -- Fix a race condition during `TMStatusChange` handling (c8249981) -- Properly transition state to disconnected (9d027394) -- Display validator status only in response to admin requests (2d6a518a) -- Add the `delivered_amount` to more RPC commands (f2756914) - - -## Version 1.2.0 - -The `rippled` 1.2.0 release introduces the MultisignReserve Amendment, which -reduces the reserve requirement associated with signer lists. This release also -includes incremental improvements to the code that handles offers. Furthermore, -`rippled` now also has the ability to automatically detect transaction -censorship attempts and issue warnings of increasing severity for transactions -that should have been included in a closed ledger after several rounds of -consensus. - -**New and Updated Features** - -- Reduce the account reserve for a Multisign SignerList (6572fc8) -- Improve transaction error condition handling (4104778) -- Allow servers to automatically detect transaction censorship attempts (945493d) -- Load validator list from file (c1a0244) -- Add RPC command shard crawl (17e0d09) -- Add RPC Call unit tests (eeb9d92) -- Grow the open ledger expected transactions quickly (7295cf9) -- Avoid dispatching multiple fetch pack threads (4dcb3c9) -- Remove unused function in AutoSocket.h (8dd8433) -- Update TxQ developer docs (e14f913) -- Add user defined literals for megabytes and kilobytes (cd1c5a3) -- Make the FeeEscalation Amendment permanent (58f786c) -- Remove undocumented experimental options from RPC sign (a96cb8f) -- Improve RPC error message for fee command (af1697c) -- Improve ledger_entry command’s inconsistent behavior (63e167b) - -**Bug Fixes** - -- Accept redirects from validator list sites (7fe1d4b) -- Implement missing string conversions for JSON (c0e9418) -- Eliminate potential undefined behavior (c71eb45) -- Add safe_cast to sure no overflow in casts between enums and integral types (a7e4541) - -## Version 1.1.2 - -The `rippled` 1.1.2 release introduces a fix for an issue that could have -prevented cluster peers from successfully bypassing connection limits when -connecting to other servers on the same cluster. Additionally, it improves -logic used to determine what the preferred ledger is during suboptimal -network conditions. - -**New and Updated Features** - -This release has no new features. - -**Bug Fixes** - -- Properly bypass connection limits for cluster peers (#2795, #2796) -- Improve preferred ledger calculation (#2784) - -## Version 1.1.1 - -The `rippled` 1.1.1 release adds support for redirections when retrieving -validator lists and changes the way that validators with an expired list -behave. Additionally, informational commands return more useful information -to allow server operators to determine the state of their server - -**New and Updated Features** - -- Enhance status reporting when using the `server_info` and `validators` commands (#2734) -- Accept redirects from validator list sites: (#2715) - -**Bug Fixes** - -- Properly handle expired validator lists when validating (#2734) - - - -## Version 1.1.0 - -The `rippled` 1.1.0 release release includes the `DepositPreAuth` amendment, which combined with the previously released `DepositAuth` amendment, allows users to pre-authorize incoming transactions to accounts, by whitelisting sender addresses. The 1.1.0 release also includes incremental improvements to several previously released features (`fix1515` amendment), deprecates support for the `sign` and `sign_for` commands from the rippled API and improves invariant checking for enhanced security. - -Ripple recommends that all server operators upgrade to XRP Ledger version 1.1.0 by Thursday, 2018-09-27, to ensure service continuity. - -**New and Updated Features** - -- Add `DepositPreAuth` ledger type and transaction (#2513) -- Increase fault tolerance and raise validation quorum to 80%, which fixes issue 2604 (#2613) -- Support ipv6 for peer and RPC comms (#2321) -- Refactor ledger replay logic (#2477) -- Improve Invariant Checking (#2532) -- Expand SQLite potential storage capacity (#2650) -- Replace UptimeTimer with UptimeClock (#2532) -- Don’t read Amount field if it is not present (#2566) -- Remove Transactor:: mFeeDue member variable (#2586) -- Remove conditional check for using Boost.Process (#2586) -- Improve charge handling in NoRippleCheckLimits test (#2629) -- Migrate more code into the chrono type system (#2629) -- Supply ConsensusTimer with milliseconds for finer precision (#2629) -- Refactor / modernize Cmake (#2629) -- Add delimiter when appending to cmake_cxx_flags (#2650) -- Remove using namespace declarations at namespace scope in headers (#2650) - -**Bug Fixes** - -- Deprecate the ‘sign’ and ‘sign_for’ APIs (#2657) -- Use liquidity from strands that consume too many offers, which will be enabled on fix1515 Amendment (#2546) -- Fix a corner case when decoding base64 (#2605) -- Trim space in Endpoint::from_string (#2593) -- Correctly suppress sent messages (#2564) -- Detect when a unit test child process crashes (#2415) -- Handle WebSocket construction exceptions (#2629) -- Improve JSON exception handling (#2605) -- Add missing virtual destructors (#2532) - - -## Version 1.0.0. - -The `rippled` 1.0.0 release includes incremental improvements to several previously released features. - -**New and Updated Features** - -- The **history sharding** functionality has been improved. Instances can now use the shard store to satisfy ledger requests. -- Change permessage-deflate and compress defaults (RIPD-506) -- Update validations on UNL change (RIPD-1566) - -**Bug Fixes** - -- Add `check`, `escrow`, and `pay_chan` to `ledger_entry` (RIPD-1600) -- Clarify Escrow semantics (RIPD-1571) - - -## Version 0.90.1 - -The `rippled` 0.90.1 release includes fixes for issues reported by external security researchers. These issues, when exploited, could cause a rippled instance to restart or, in some circumstances, stop executing. While these issues can result in a denial of service attack, none affect the integrity of the XRP Ledger and no user funds, including XRP, are at risk. - -**New and Updated Features** - -This release has no new features. - -**Bug Fixes** - -- Address issues identified by external review: - - Verify serialized public keys more strictly before using them - (RIPD-1617, RIPD-1619, RIPD-1621) - - Eliminate a potential out-of-bounds memory access in the base58 - encoding/decoding logic (RIPD-1618) - - Avoid invoking undefined behavior in memcpy (RIPD-1616) - - Limit STVar recursion during deserialization (RIPD-1603) -- Use lock when creating a peer shard rangeset - - -## Version 0.90.0 - -The `rippled` 0.90.0 release introduces several features and enhancements that improve the reliability, scalability and security of the XRP Ledger. - -Highlights of this release include: - -- The `DepositAuth` amendment, which lets an account strictly reject any incoming money from transactions sent by other accounts. -- The `Checks` amendment, which allows users to create deferred payments that can be cancelled or cashed by their intended recipients. -- **History Sharding**, which allows `rippled` servers to distribute historical ledger data if they agree to dedicate storage for segments of ledger history. -- New **Preferred Ledger by Branch** semantics which improve the logic that allow a server to decide which ledger it should base future ledgers on when there are multiple candidates. - -**New and Updated Features** - -- Add support for Deposit Authorization account root flag ([#2239](https://github.com/ripple/rippled/issues/2239)) -- Implement history shards ([#2258](https://github.com/ripple/rippled/issues/2258)) -- Preferred ledger by branch ([#2300](https://github.com/ripple/rippled/issues/2300)) -- Redesign Consensus Simulation Framework ([#2209](https://github.com/ripple/rippled/issues/2209)) -- Tune for higher transaction processing ([#2294](https://github.com/ripple/rippled/issues/2294)) -- Optimize queries for `account_tx` to work around SQLite query planner ([#2312](https://github.com/ripple/rippled/issues/2312)) -- Allow `Journal` to be copied/moved ([#2292](https://github.com/ripple/rippled/issues/2292)) -- Cleanly report invalid `[server]` settings ([#2305](https://github.com/ripple/rippled/issues/2305)) -- Improve log scrubbing ([#2358](https://github.com/ripple/rippled/issues/2358)) -- Update `rippled-example.cfg` ([#2307](https://github.com/ripple/rippled/issues/2307)) -- Force json commands to be objects ([#2319](https://github.com/ripple/rippled/issues/2319)) -- Fix cmake clang build for sanitizers ([#2325](https://github.com/ripple/rippled/issues/2325)) -- Allow `account_objects` RPC to filter by “check” ([#2356](https://github.com/ripple/rippled/issues/2356)) -- Limit nesting of json commands ([#2326](https://github.com/ripple/rippled/issues/2326)) -- Unit test that `sign_for` returns a correct hash ([#2333](https://github.com/ripple/rippled/issues/2333)) -- Update Visual Studio build instructions ([#2355](https://github.com/ripple/rippled/issues/2355)) -- Force boost static linking for MacOS builds ([#2334](https://github.com/ripple/rippled/issues/2334)) -- Update MacOS build instructions ([#2342](https://github.com/ripple/rippled/issues/2342)) -- Add dev docs generation to Jenkins ([#2343](https://github.com/ripple/rippled/issues/2343)) -- Poll if process is still alive in Test.py ([#2290](https://github.com/ripple/rippled/issues/2290)) -- Remove unused `beast::currentTimeMillis()` ([#2345](https://github.com/ripple/rippled/issues/2345)) - - -**Bug Fixes** -- Improve error message on mistyped command ([#2283](https://github.com/ripple/rippled/issues/2283)) -- Add missing includes ([#2368](https://github.com/ripple/rippled/issues/2368)) -- Link boost statically only when requested ([#2291](https://github.com/ripple/rippled/issues/2291)) -- Unit test logging fixes ([#2293](https://github.com/ripple/rippled/issues/2293)) -- Fix Jenkins pipeline for branches ([#2289](https://github.com/ripple/rippled/issues/2289)) -- Avoid AppVeyor stack overflow ([#2344](https://github.com/ripple/rippled/issues/2344)) -- Reduce noise in log ([#2352](https://github.com/ripple/rippled/issues/2352)) - - -## Version 0.81.0 - -The `rippled` 0.81.0 release introduces changes that improve the scalability of the XRP Ledger and transitions the recommended validator configuration to a new hosted site, as described in Ripple's [Decentralization Strategy Update](https://ripple.com/dev-blog/decentralization-strategy-update/) post. - -**New and Updated Features** - -- New hosted validator configuration. - - -**Bug Fixes** - -- Optimize queries for account_tx to work around SQLite query planner ([#2312](https://github.com/ripple/rippled/issues/2312)) - - -## Version 0.80.2 - -The `rippled` 0.80.2 release introduces changes that improve the scalability of the XRP Ledger. - -**New and Updated Features** - -This release has no new features. - -**Bug Fixes** - -- Do not dispatch a transaction received from a peer for processing if it has already been dispatched within the past ten seconds. -- Increase the number of transaction handlers that can be in flight in the job queue and decrease the relative cost for peers to share transaction and ledger data. -- Make better use of resources by adjusting the number of threads we initialize, by reverting commit [#68b8ffd](https://github.com/ripple/rippled/commit/68b8ffdb638d07937f841f7217edeb25efdb3b5d). - -## Version 0.80.1 - -The `rippled` 0.80.1 release provides several enhancements in support of published validator lists and corrects several bugs. - -**New and Updated Features** - -- Allow including validator manifests in published list ([#2278](https://github.com/ripple/rippled/issues/2278)) -- Add validator list RPC commands ([#2242](https://github.com/ripple/rippled/issues/2242)) -- Support [SNI](https://en.wikipedia.org/wiki/Server_Name_Indication) when querying published list sites and use Windows system root certificates ([#2275](https://github.com/ripple/rippled/issues/2275)) -- Grow TxQ expected size quickly, shrink slowly ([#2235](https://github.com/ripple/rippled/issues/2235)) - -**Bug Fixes** - -- Make consensus quorum unreachable if validator list expires ([#2240](https://github.com/ripple/rippled/issues/2240)) -- Properly use ledger hash to break ties when determing working ledger for consensus ([#2257](https://github.com/ripple/rippled/issues/2257)) -- Explictly use std::deque for missing node handler in SHAMap code ([#2252](https://github.com/ripple/rippled/issues/2252)) -- Verify validator token manifest matches private key ([#2268](https://github.com/ripple/rippled/issues/2268)) - - -## Version 0.80.0 - -The `rippled` 0.80.0 release introduces several enhancements that improve the reliability, scalability and security of the XRP Ledger. - -Highlights of this release include: - -- The `SortedDirectories` amendment, which allows the entries stored within a page to be sorted, and corrects a technical flaw that could, in some edge cases, prevent an empty intermediate page from being deleted. -- Changes to the UNL and quorum rules - + Use a fixed size UNL if the total listed validators are below threshold - + Ensure a quorum of 0 cannot be configured - + Set a quorum to provide Byzantine fault tolerance until a threshold of total validators is exceeded, at which time the quorum is 80% - -**New and Updated Features** - -- Improve directory insertion and deletion ([#2165](https://github.com/ripple/rippled/issues/2165)) -- Move consensus thread safety logic from the generic implementation in Consensus into the RCL adapted version RCLConsensus ([#2106](https://github.com/ripple/rippled/issues/2106)) -- Refactor Validations class into a generic version that can be adapted ([#2084](https://github.com/ripple/rippled/issues/2084)) -- Make minimum quorum Byzantine fault tolerant ([#2093](https://github.com/ripple/rippled/issues/2093)) -- Make amendment blocked state thread-safe and simplify a constructor ([#2207](https://github.com/ripple/rippled/issues/2207)) -- Use ledger hash to break ties ([#2169](https://github.com/ripple/rippled/issues/2169)) -- Refactor RangeSet ([#2113](https://github.com/ripple/rippled/issues/2113)) - -**Bug Fixes** - -- Fix an issue where `setAmendmentBlocked` is only called when processing the `EnableAmendment` transaction for the amendment ([#2137](https://github.com/ripple/rippled/issues/2137)) -- Track escrow in recipient's owner directory ([#2212](https://github.com/ripple/rippled/issues/2212)) - -## Version 0.70.2 - -The `rippled` 0.70.2 release corrects an emergent behavior which causes large numbers of transactions to get -stuck in different nodes' open ledgers without being passed on to validators, resulting in a spike in the open -ledger fee on those nodes. - -**New and Updated Features** - -This release has no new features. - -**Bug Fixes** - -- Recent fee rises and TxQ issues ([#2215](https://github.com/ripple/rippled/issues/2215)) - - -## Version 0.70.1 - -The `rippled` 0.70.1 release corrects a technical flaw in the newly refactored consensus code that could cause a node to get stuck in consensus due to stale votes from a -peer, and allows compiling `rippled` under the 1.1.x releases of OpenSSL. - -**New and Updated Features** - -This release has no new features. - -**Bug Fixes** - -- Allow compiling against OpenSSL 1.1.0 ([#2151](https://github.com/ripple/rippled/pull/2151)) -- Log invariant check messages at "fatal" level ([2154](https://github.com/ripple/rippled/pull/2154)) -- Fix the consensus code to update all disputed transactions after a node changes a position ([2156](https://github.com/ripple/rippled/pull/2156)) - - -## Version 0.70.0 - -The `rippled` 0.70.0 release introduces several enhancements that improve the reliability, scalability and security of the network. - -Highlights of this release include: - -- The `FlowCross` amendment, which streamlines offer crossing and autobrigding logic by leveraging the new “Flow” payment engine. -- The `EnforceInvariants` amendment, which can safeguard the integrity of the XRP Ledger by introducing code that executes after every transaction and ensures that the execution did not violate key protocol rules. -- `fix1373`, which addresses an issue that would cause payments with certain path specifications to not be properly parsed. - -**New and Updated Features** - -- Implement and test invariant checks for transactions (#2054) -- TxQ: Functionality to dump all queued transactions (#2020) -- Consensus refactor for simulation/cleanup (#2040) -- Payment flow code should support offer crossing (#1884) -- make `Config` init extensible via lambda (#1993) -- Improve Consensus Documentation (#2064) -- Refactor Dependencies & Unit Test Consensus (#1941) -- `feature` RPC test (#1988) -- Add unit Tests for handlers/TxHistory.cpp (#2062) -- Add unit tests for handlers/AccountCurrenciesHandler.cpp (#2085) -- Add unit test for handlers/Peers.cpp (#2060) -- Improve logging for Transaction affects no accounts warning (#2043) -- Increase logging in PeerImpl fail (#2043) -- Allow filtering of ledger objects by type in RPC (#2066) - -**Bug Fixes** - -- Fix displayed warning when generating brain wallets (#2121) -- Cmake build does not append '+DEBUG' to the version info for non-unity builds -- Crossing tiny offers can misbehave on RCL -- `asfRequireAuth` flag not always obeyed (#2092) -- Strand creating is incorrectly accepting invalid paths -- JobQueue occasionally crashes on shutdown (#2025) -- Improve pseudo-transaction handling (#2104) - -## Version 0.60.3 - -The `rippled` 0.60.3 release helps to increase the stability of the network under heavy load. - -**New and Updated Features** - -This release has no new features. - -**Bug Fixes** - -Server overlay improvements ([#2110](https://github.com/ripple/rippled/pull/2011)) - -## Version 0.60.2 - -The `rippled` 0.60.2 release further strengthens handling of cases associated with a previously patched exploit, in which NoRipple flags were being bypassed by using offers. - -**New and Updated Features** - -This release has no new features. - -**Bug Fixes** - -Prevent the ability to bypass the `NoRipple` flag using offers ([#7cd4d78](https://github.com/ripple/rippled/commit/4ff40d4954dfaa237c8b708c2126cb39566776da)) - -## Version 0.60.1 - -The `rippled` 0.60.1 release corrects a technical flaw that resulted from using 32-bit space identifiers instead of the protocol-defined 16-bit values for Escrow and Payment Channel ledger entries. rippled version 0.60.1 also fixes a problem where the WebSocket timeout timer would not be cancelled when certain errors occurred during subscription streams. Ripple requires upgrading to rippled version 0.60.1 immediately. - -**New and Updated Feature** - -This release has no new features. - -**Bug Fixes** - -Correct calculation of Escrow and Payment Channel indices. -Fix WebSocket timeout timer issues. - -## Version 0.60.0 - -The `rippled` 0.60.0 release introduces several enhancements that improve the reliability and scalability of the Ripple Consensus Ledger (RCL), including features that add ledger interoperability by improving Interledger Protocol compatibility. Ripple recommends that all server operators upgrade to version 0.60.0 by Thursday, 2017-03-30, for service continuity. - -Highlights of this release include: - -- `Escrow` (previously called `SusPay`) which permits users to cryptographically escrow XRP on RCL with an expiration date, and optionally a hashlock crypto-condition. Ripple expects Escrow to be enabled via an Amendment named [`Escrow`](https://ripple.com/build/amendments/#escrow) on Thursday, 2017-03-30. See below for details. -- Dynamic UNL Lite, which allows `rippled` to automatically adjust which validators it trusts based on recommended lists from trusted publishers. - -**New and Updated Features** - -- Add `Escrow` support (#2039) -- Dynamize trusted validator list and quorum (#1842) -- Simplify fee handling during transaction submission (#1992) -- Publish server stream when fee changes (#2016) -- Replace manifest with validator token (#1975) -- Add validator key revocations (#2019) -- Add `SecretKey` comparison operator (#2004) -- Reduce `LEDGER_MIN_CONSENSUS` (#2013) -- Update libsecp256k1 and Beast B30 (#1983) -- Make `Config` extensible via lambda (#1993) -- WebSocket permessage-deflate integration (#1995) -- Do not close socket on a foreign thread (#2014) -- Update build scripts to support latest boost and ubuntu distros (#1997) -- Handle protoc targets in scons ninja build (#2022) -- Specify syntax version for ripple.proto file (#2007) -- Eliminate protocol header dependency (#1962) -- Use gnu gold or clang lld linkers if available (#2031) -- Add tests for `lookupLedger` (#1989) -- Add unit test for `get_counts` RPC method (#2011) -- Add test for `transaction_entry` request (#2017) -- Unit tests of RPC "sign" (#2010) -- Add failure only unit test reporter (#2018) - -**Bug Fixes** - -- Enforce rippling constraints during payments (#2049) -- Fix limiting step re-execute bug (#1936) -- Make "wss" work the same as "wss2" (#2033) -- Config test uses unique directories for each test (#1984) -- Check for malformed public key on payment channel (#2027) -- Send a websocket ping before timing out in server (#2035) - - -## Version 0.50.3 - -The `rippled` 0.50.3 release corrects a reported exploit that would allow a combination of trust lines and order books in a payment path to bypass the blocking effect of the [`NoRipple`](https://ripple.com/build/understanding-the-noripple-flag/) flag. Ripple recommends that all server operators immediately upgrade to version 0.50.3. - -**New and Updated Features** - -This release has no new features. - -**Bug Fixes** - -Correct a reported exploit that would allow a combination of trust lines and order books in a payment path to bypass the blocking effect of the “NoRipple” flag. - - -## Version 0.50.2 - -The `rippled` 0.50.2 release adjusts the default TLS cipher list and corrects a flaw that would not allow an SSL handshake to properly complete if the port was configured using the `wss` keyword. Ripple recommends upgrading to 0.50.2 only if server operators are running rippled servers that accept client connections over TLS. - -**New and Updated Features** - -This release has no new features. - -**Bug Fixes** - -Adjust the default cipher list and correct a flaw that would not allow an SSL handshake to properly complete if the port was configured using the `wss` keyword (#1985) - - -## Version 0.50.0 - -The `rippled` 0.50.0 release includes TickSize, which allows gateways to set a "tick size" for assets they issue to help promote faster price discovery and deeper liquidity, as well as reduce transaction spam and ledger churn on RCL. Ripple expects TickSize to be enabled via an Amendment called TickSize on Tuesday, 2017-02-21. - -You can [update to the new version](https://ripple.com/build/rippled-setup/#updating-rippled) on Red Hat Enterprise Linux 7 or CentOS 7 using yum. For other platforms, please [compile the new version from source](https://wiki.ripple.com/Rippled_build_instructions). - -**New and Updated Features** - -**Tick Size** - -Currently, offers on RCL can differ by as little as one part in a quadrillion. This means that there is essentially no value to placing an offer early, as an offer placed later at a microscopically better price gets priority over it. The [TickSize](https://ripple.com/build/amendments/#ticksize) Amendment solves this problem by introducing a minimum tick size that a price must move for an offer to be considered to be at a better price. The tick size is controlled by the issuers of the assets involved. - -This change lets issuers quantize the exchange rates of offers to use a specified number of significant digits. Gateways must enable a TickSize on their account for this feature to benefit them. A single AccountSet transaction may set a `TickSize` parameter. Legal values are 0 and 3-15 inclusive. Zero removes the setting. 3-15 allow that many decimal digits of precision in the pricing of offers for assets issued by this account. It will still be possible to place an offer to buy or sell any amount of an asset and the offer will still keep that amount as exactly as it does now. If an offer involves two assets that each have a tick size, the smaller number of significant figures (larger ticks) controls. - -For asset pairs with XRP, the tick size imposed, if any, is the tick size of the issuer of the non-XRP asset. For asset pairs without XRP, the tick size imposed, if any, is the smaller of the two issuer's configured tick sizes. - -The tick size is imposed by rounding the offer quality down to the nearest tick and recomputing the non-critical side of the offer. For a buy, the amount offered is rounded down. For a sell, the amount charged is rounded up. - -The primary expected benefit of the TickSize amendment is the reduction of bots fighting over the tip of the order book, which means: -- Quicker price discovery as outpricing someone by a microscopic amount is made impossible (currently bots can spend hours outbidding each other with no significant price movement) -- A reduction in offer creation and cancellation spam -- Traders can't outbid by a microscopic amount -- More offers left on the books as priority - -We also expect larger tick sizes to benefit market makers in the following ways: -- They increase the delta between the fair market value and the trade price, ultimately reducing spreads -- They prevent market makers from consuming each other's offers due to slight changes in perceived fair market value, which promotes liquidity -- They promote faster price discovery by reducing the back and forths required to move the price by traders who don't want to move the price more than they need to -- They reduce transaction spam by reducing fighting over the tip of the order book and reducing the need to change offers due to slight price changes -- They reduce ledger churn and metadata sizes by reducing the number of indexes each order book must have -- They allow the order book as presented to traders to better reflect the actual book since these presentations are inevitably aggregated into ticks - -**Hardened TLS configuration** - -This release updates the default TLS configuration for rippled. The new release supports only 2048-bit DH parameters and defines a new default set of modern ciphers to use, removing support for ciphers and hash functions that are no longer considered secure. - -Server administrators who wish to have different settings can configure custom global and per-port cipher suites in the configuration file using the `ssl_ciphers` directive. - -**0.50.0 Change Log** - -Remove websocketpp support (#1910) - -Increase OpenSSL requirements & harden default TLS cipher suites (#1913) - -Move test support sources out of ripple directory (#1916) - -Enhance ledger header RPC commands (#1918) - -Add support for tick sizes (#1922) - -Port discrepancy-test.coffee to c++ (#1930) - -Remove redundant call to `clearNeedNetworkLedger` (#1931) - -Port freeze-test.coffee to C++ unit test. (#1934) - -Fix CMake docs target to work if `BOOST_ROOT` is not set (#1937) - -Improve setup for account_tx paging test (#1942) - -Eliminate npm tests (#1943) - -Port uniport js test to cpp (#1944) - -Enable amendments in genesis ledger (#1944) - -Trim ledger data in Discrepancy_test (#1948) - -Add `current_ledger` field to `fee` result (#1949) - -Cleanup unit test support code (#1953) - -Add ledger save / load tests (#1955) - -Remove unused websocket files (#1957) - -Update RPC handler role/usage (#1966) - -**Bug Fixes** - -Validator's manifest not forwarded beyond directly connected peers (#1919) - -**Upcoming Features** - -We expect the previously announced Suspended Payments feature, which introduces new transaction types to the Ripple protocol that will permit users to cryptographically escrow XRP on RCL, to be enabled via the [SusPay](https://ripple.com/build/amendments/#suspay) Amendment on Tuesday, 2017-02-21. - -Also, we expect support for crypto-conditions, which are signature-like structures that can be used with suspended payments to support ILP integration, to be included in the next rippled release scheduled for March. - -Lastly, we do not have an update on the previously announced changes to the hash tree structure that rippled uses to represent a ledger, called [SHAMapV2](https://ripple.com/build/amendments/#shamapv2). At the time of activation, this amendment will require brief scheduled allowable unavailability while the changes to the hash tree structure are computed by the network. We will keep the community updated as we progress towards this date (TBA). - - -## Version 0.40.1 - -The `rippled` 0.40.1 release increases SQLite database limits in all rippled servers. Ripple recommends upgrading to 0.40.1 only if server operators are running rippled servers with full-history of the ledger. There are no new or updated features in the 0.40.1 release. - -You can update to the new version on Red Hat Enterprise Linux 7 or CentOS 7 using yum. For other platforms, please compile the new version from source. - -**New and Updated Features** - -This release has no new features. - -**Bug Fixes** - -Increase SQLite database limits to prevent full-history servers from crashing when restarting. (#1961) - -## Version 0.40.0 - -The `rippled` 0.40.0 release includes Suspended Payments, a new transaction type on the Ripple network that functions similar to an escrow service, which permits users cryptographically escrow XRP on RCL with an expiration date. Ripple expects Suspended Payments to be enabled via an Amendment named [SusPay](https://ripple.com/build/amendments/#suspay) on Tuesday, 2017-01-17. - -You can update to the new version on Red Hat Enterprise Linux 7 or CentOS 7 using yum. For other platforms, please compile the new version from source. - -**New and Updated Features** - -Previously, Ripple announced the introduction of Payment Channels during the release of rippled version 0.33.0, which permit scalable, off-ledger checkpoints of high volume, low value payments flowing in a single direction. This was the first step in a multi-phase effort to make RCL more scalable and to support Interledger Protocol (ILP). Ripple expects Payment Channels to be enabled via an Amendment called [PayChan](https://ripple.com/build/amendments/#paychan) on a future date to be determined. - -In the second phase towards making RCL more scalable and compatible with ILP, Ripple is introducing Suspended Payments, a new transaction type on the Ripple network that functions similar to an escrow service, which permits users to cryptographically escrow XRP on RCL with an expiration date. Ripple expects Suspended Payments to be enabled via an Amendment named [SusPay](https://ripple.com/build/amendments/#suspay) on Tuesday, 2017-01-17. - -A Suspended Payment can be created, which deducts the funds from the sending account. It can then be either fulfilled or canceled. It can only be fulfilled if the fulfillment transaction makes it into a ledger with a CloseTime lower than the expiry date of the transaction. It can be canceled with a transaction that makes it into a ledger with a CloseTime greater than the expiry date of the transaction. - -In the third phase towards making RCL more scalable and compatible with ILP, Ripple plans to introduce additional library support for crypto-conditions, which are distributable event descriptions written in a standard format that describe how to recognize a fulfillment message without saying exactly what the fulfillment is. Fulfillments are cryptographically verifiable messages that prove an event occurred. If you transmit a fulfillment, then everyone who has the condition can agree that the condition has been met. Fulfillment requires the submission of a signature that matches the condition (message hash and public key). This format supports multiple algorithms, including different hash functions and cryptographic signing schemes. Crypto-conditions can be nested in multiple levels, with each level possibly having multiple signatures. - -Lastly, we do not have an update on the previously announced changes to the hash tree structure that rippled uses to represent a ledger, called [SHAMapV2](https://ripple.com/build/amendments/#shamapv2). This will require brief scheduled allowable downtime while the changes to the hash tree structure are propagated by the network. We will keep the community updated as we progress towards this date (TBA). - -Consensus refactor (#1874) - -Bug Fixes - -Correct an issue in payment flow code that did not remove an unfunded offer (#1860) - -Sign validator manifests with both ephemeral and master keys (#1865) - -Correctly parse multi-buffer JSON messages (#1862) - - -## Version 0.33.0 - -The `rippled` 0.33.0 release includes an improved version of the payment code, which we expect to be activated via Amendment on Wednesday, 2016-10-20 with the name [Flow](https://ripple.com/build/amendments/#flow). We are also introducing XRP Payment Channels, a new structure in the ledger designed to support [Interledger Protocol](https://interledger.org/) trust lines as balances get substantial, which we expect to be activated via Amendment on a future date (TBA) with the name [PayChan](https://ripple.com/build/amendments/#paychan). Lastly, we will be introducing changes to the hash tree structure that rippled uses to represent a ledger, which we expect to be available via Amendment on a future date (TBA) with the name [SHAMapV2](https://ripple.com/build/amendments/#shamapv2). - -You can [update to the new version](https://ripple.com/build/rippled-setup/#updating-rippled) on Red Hat Enterprise Linux 7 or CentOS 7 using yum. For other platforms, please [compile the new version from source](https://wiki.ripple.com/Rippled_build_instructions). - -** New and Updated Features ** - -A fixed version of the new payment processing engine, which we initially announced on Friday, 2016-07-29, is expected to be available via Amendment on Wednesday, 2016-10-20 with the name [Flow](https://ripple.com/build/amendments/#flow). The new payments code adds no new features, but improves efficiency and robustness in payment handling. - -The Flow code may occasionally produce slightly different results than the old payment processing engine due to the effects of floating point rounding. - -We will be introducing changes to the hash tree structure that rippled uses to represent a ledger, which we expect to be activated via Amendment on a future date (TBA) with the name [SHAMapV2](https://ripple.com/build/amendments/#shamapv2). The new structure is more compact and efficient than the previous version. This affects how ledger hashes are calculated, but has no other user-facing consequences. The activation of the SHAMapV2 amendment will require brief scheduled allowable downtime, while the changes to the hash tree structure are propagated by the network. We will keep the community updated as we progress towards this date (TBA). - -In an effort to make RCL more scalable and to support Interledger Protocol (ILP) trust lines as balances get more substantial, we’re introducing XRP Payment Channels, a new structure in the ledger, which we expect to be available via Amendment on a future date (TBA) with the name [PayChan](https://ripple.com/build/amendments/#paychan). - -XRP Payment Channels permit scalable, intermittent, off-ledger settlement of ILP trust lines for high volume payments flowing in a single direction. For bidirectional channels, an XRP Payment Channel can be used in each direction. The recipient can claim any unpaid balance at any time. The owner can top off the channel as needed. The owner must wait out a delay to close the channel to give the recipient a chance to supply any claims. The total amount paid increases monotonically as newer claims are issued. - -The initial concept behind payment channels was discussed as early as 2011 and the first implementation was done by Mike Hearn in bitcoinj. Recent work being done by Lightning Network has showcased examples of the many use cases for payment channels. The introduction of XRP Payment Channels allows for a more efficient integration between RCL and ILP to further support enterprise use cases for high volume payments. - -Added `getInfoRippled.sh` support script to gather health check for rippled servers [RIPD-1284] - -The `account_info` command can now return information about queued transactions - [RIPD-1205] - -Automatically-provided sequence numbers now consider the transaction queue - [RIPD-1206] - -The `server_info` and `server_state` commands now include the queue-related escalated fee factor in the load_factor field of the response - [RIPD-1207] - -A transaction with a high transaction cost can now cause transactions from the same sender queued in front of it to get into the open ledger if the transaction costs are high enough on average across all such transactions. - [RIPD-1246] - -Reorganization: Move `LoadFeeTrack` to app/tx and clean up functions - [RIPD-956] - -Reorganization: unit test source files - [RIPD-1132] - -Reorganization: NuDB stand-alone repository - [RIPD-1163] - -Reorganization: Add `BEAST_EXPECT` to Beast - [RIPD-1243] - -Reorganization: Beast 64-bit CMake/Bjam target on Windows - [RIPD-1262] - -** Bug Fixes ** - -`PaymentSandbox::balanceHook` can return the wrong issuer, which could cause the transfer fee to be incorrectly by-passed in rare circumstances. [RIPD-1274, #1827] - -Prevent concurrent write operations in websockets [#1806] - -Add HTTP status page for new websocket implementation [#1855] - - -## Version 0.32.1 - -The `rippled` 0.32.1 release includes an improved version of the payment code, which we expect to be available via Amendment on Wednesday, 2016-08-24 with the name FlowV2, and a completely new implementation of the WebSocket protocol for serving clients. - -You can [update to the new version](https://ripple.com/build/rippled-setup/#updating-rippled) on Red Hat Enterprise Linux 7 or CentOS 7 using yum. For other platforms, please [compile the new version from source](https://wiki.ripple.com/Rippled_build_instructions). - -**New and Updated Features** - -An improved version of the payment processing engine, which we expect to be available via Amendment on Wednesday, 2016-08-24 with the name “FlowV2”. The new payments code adds no new features, but improves efficiency and robustness in payment handling. - -The FlowV2 code may occasionally produce slightly different results than the old payment processing engine due to the effects of floating point rounding. Once FlowV2 is enabled on the network then old servers without the FlowV2 amendment will lose sync more frequently because of these differences. - -**Beast WebSocket** - -A completely new implementation of the WebSocket protocol for serving clients is available as a configurable option for `rippled` administrators. To enable this new implementation, change the “protocol” field in `rippled.cfg` from “ws” to “ws2” (or from “wss” to “wss2” for Secure WebSockets), as illustrated in this example: - - [port_ws_public] - port = 5006 - ip = 0.0.0.0 - protocol = wss2 - -The new implementation paves the way for increased reliability and future performance when submitting commands over WebSocket. The behavior and syntax of commands should be identical to the previous implementation. Please report any issues to support@ripple.com. A future version of rippled will remove the old WebSocket implementation, and use only the new one. - -**Bug fixes** - -Fix a non-exploitable, intermittent crash in some client pathfinding requests (RIPD-1219) - -Fix a non-exploitable crash caused by a race condition in the HTTP server. (RIPD-1251) - -Fix bug that could cause a previously fee queued transaction to not be relayed after being in the open ledger for an extended time without being included in a validated ledger. Fix bug that would allow an account to have more than the allowed limit of transactions in the fee queue. Fix bug that could crash debug builds in rare cases when replacing a dropped transaction. (RIPD-1200) - -Remove incompatible OS X switches in Test.py (RIPD-1250) - -Autofilling a transaction fee (sign / submit) with the experimental `x-queue-okay` parameter will use the user’s maximum fee if the open ledger fee is higher, improving queue position, and giving the tx more chance to succeed. (RIPD-1194) - - - -## Version 0.32.0 - -The `rippled` 0.32.0 release improves transaction queue which now supports batching and can hold up to 10 transactions per account, allowing users to queue multiple transactions for processing when the network load is high. Additionally, the `server_info` and `server_state` commands now include information on transaction cost multipliers and the fee command is available to unprivileged users. We advise rippled operators to upgrade immediately. - -You can update to the new version on Red Hat Enterprise Linux 7 or CentOS 7 using yum. For other platforms, please compile the new version from source. - -**New and Updated Features** - -- Allow multiple transactions per account in transaction queue (RIPD-1048). This also introduces a new transaction engine code, `telCAN_NOT_QUEUE`. -- Charge pathfinding consumers per source currency (RIPD-1019): The IP address used to perform pathfinding operations is now charged an additional resource increment for each source currency in the path set. -- New implementation of payment processing engine. This implementation is not yet enabled by default. -- Include amendments in validations subscription -- Add C++17 compatibility -- New WebSocket server implementation with Beast.WebSocket library. The new library offers a stable, high-performance websocket server implementation. To take advantage of this implementation, change websocket protocol under rippled.cfg from wss and ws to wss2 and ws2 under `[port_wss_admin]` and `[port_ws_public]` stanzas: -``` - [port_wss_admin] - port = 51237 - ip = 127.0.0.1 - admin = 127.0.0.1 - protocol = wss2 - - [port_ws_public] - port = 51233 - ip = 0.0.0.0 - protocol = wss2, ws2 -``` -- The fee command is now public (RIPD-1113) -- The fee command checks open ledger rules (RIPD-1183) -- Log when number of available file descriptors is insufficient (RIPD-1125) -- Publish all validation fields for signature verification -- Get quorum and trusted master validator keys from validators.txt -- Standalone mode uses temp DB files by default (RIPD-1129): If a [database_path] is configured, it will always be used, and tables will be upgraded on startup. -- Include config manifest in server_info admin response (RIPD-1172) - -**Bug fixes** - -- Fix history acquire check (RIPD-1112) -- Correctly handle connections that fail security checks (RIPD-1114) -- Fix secured Websocket closing -- Reject invalid MessageKey in SetAccount handler (RIPD-308, RIPD-990) -- Fix advisory delete effect on history acquisition (RIPD-1112) -- Improve websocket send performance (RIPD-1158) -- Fix XRP bridge payment bug (RIPD-1141) -- Improve error reporting for wallet_propose command. Also include a warning if the key used may be an insecure, low-entropy key. (RIPD-1110) - -**Deprecated features** - -- Remove obsolete sendGetPeers support (RIPD-164) -- Remove obsolete internal command (RIPD-888) - - - - -## Version 0.31.2 - -The `rippled` 0.31.2 release corrects issues with the fee escalation algorithm. We advise `rippled` operators to upgrade immediately. - -You can [update to the new version](https://ripple.com/build/rippled-setup/#updating-rippled) on Red Hat Enterprise Linux 7 or CentOS 7 using yum. For other platforms, please [compile the new version from source](https://wiki.ripple.com/Rippled_build_instructions). - -**New and Updated Features** - -This release has no new features. - -**Bug Fixes** - -- A defect in the fee escalation algorithm that caused network fees to escalate more rapidly than intended has been corrected. (RIPD-1177) -- The minimum local fee advertised by validators will no longer be adjusted upwards. - - - -## Version 0.31.1 - -The `rippled` 0.31.1 release contains one important bug fix. We advise `rippled` operators to upgrade immediately. - -You can [update to the new version](https://ripple.com/build/rippled-setup/#updating-rippled) on Red Hat Enterprise Linux 7 or CentOS 7 using yum. For other platforms, please [compile the new version from source](https://wiki.ripple.com/Rippled_build_instructions). - -**New and Updated Features** - -This release has no new features. - -**Bug Fixes** - -`rippled` 0.31.1 contains the following fix: - -- Correctly handle ledger validations with no `LedgerSequence` field. Previous versions of `rippled` incorrectly assumed that the optional validation field would always be included. Current versions of the software always include the field, and gracefully handle its absence. - - - -## Version 0.31.0 - -`rippled` 0.31.0 has been released. - -You can [update to the new version](https://ripple.com/build/rippled-setup/#updating-rippled) on Red Hat Enterprise Linux 7 or CentOS 7 using yum. - -For other platforms, please [compile the new version from source](https://wiki.ripple.com/Rippled_build_instructions). Use the `git log` command to confirm you have the correct source tree. The first log entry should be the change setting the version: - - - commit a5d58566386fd86ae4c816c82085fe242b255d2c - Author: Nik Bougalis - Date: Sun Apr 17 18:02:02 2016 -0700 - - Set version to 0.31.0 - - -**Warnings** - -Please expect a one-time delay when starting 0.31.0 while certain database indices are being built or rebuilt. The delay can be up to five minutes, during which CPU will spike and the server will appear unresponsive (no response to RPC, etc.). - -Additionally, `rippled` 0.31.0 now checks at start-up time that it has sufficient open file descriptors available, and shuts down with an error message if it does not. Previous versions of `rippled` could run out of file descriptors unexpectedly during operation. If you get a file-descriptor error message, increase the number of file descriptors available to `rippled` (for example, editing `/etc/security/limits.conf`) and restart. - -**New and Updated Features** - -`rippled` 0.31.0 has the following new or updated features: - -- (New) [**Amendments**](https://ripple.com/build/amendments/) - A consensus-based system for introducing changes to transaction processing. -- (New) [**Multi-Signing**](https://ripple.com/build/transactions/#multi-signing) - (To be enabled as an amendment) Allow transactions to be authorized by a list of signatures. (RIPD-182) -- (New) **Transaction queue and FeeEscalation** - (To be enabled as an amendment) Include or defer transactions based on the [transaction cost](https://ripple.com/build/transaction-cost/) offered, for better behavior in DDoS conditions. (RIPD-598) -- (Updated) Validations subscription stream now includes `ledger_index` field. (DEC-564) -- (Updated) You can request SignerList information in the `account_info` command (RIPD-1061) - -**Closed Issues** - -`rippled` 0.31.0 has the following fixes and improvements: - -- Improve held transaction submission -- Update SQLite from 3.8.11.1 to 3.11.0 -- Allow random seed with specified wallet_propose key_type (RIPD-1030) -- Limit pathfinding source currency limits (RIPD-1062) -- Speed up out of order transaction processing (RIPD-239) -- Pathfinding optimizations -- Streamlined UNL/validator list: The new code removes the ability to specify domain names in the [validators] configuration block, and no longer supports the [validators_site] option. -- Add websocket client -- Add description of rpcSENDMAX_MALFORMED error -- Convert PathRequest to use std::chrono (RIPD-1069) -- Improve compile-time OpenSSL version check -- Clear old Validations during online delete (RIPD-870) -- Return correct error code during unfunded offer cross (RIPD-1082) -- Report delivered_amount for legacy account_tx queries. -- Improve error message when signing fails (RIPD-1066) -- Fix websocket deadlock - - - - -## Version 0.30.1 - -rippled 0.30.1 has been released. The commit can be found on GitHub at: - -Prior to building, please confirm you have the correct source tree with the **git log** command. The first log entry should be the change setting the version: - - commit c717006c44126aa0edb3a36ca29ee30e7a72c1d3 - Author: Nik Bougalis - Date: Wed Feb 3 14:49:07 2016 -0800 - - Set version to 0.30.1 - -This release incorporates a number of important features, bugfixes and functional improvements. Please refer to the [Git commit history](https://github.com/ripple/rippled/commits/0.30.1) for more detailed information. - -**Release Overview** - -The rippled team is proud to release rippled version 0.30.1. This version contains a several minor new features as well as significant improvements to the consensus algorithm that make it work faster and with more consistency. In the time we have been testing the new release on our validators, these changes have led to increased agreement and shorter close times between ledger versions, for approximately 40% more ledgers validated per day. - -**New Features** - -- Secure gateway: configured IPs can forward identifying user data in HTTP headers, including user name and origin IP. If the user name exists, then resource limits are lifted for that session. See rippled-example.cfg for more information. -- Allow fractional fee multipliers (RIPD-626) -- Add “expiration” to account\_offers (RIPD-1049) -- Add “owner\_funds” to “transactions” array in RPC ledger (RIPD-1050) -- Add "tx" option to "ledger" command line -- Add server uptime in server\_info -- Allow multiple incoming connections from the same IP -- Report connection uptime in peer command (RIPD-927) -- Permit pathfinding to be disabled by setting \[path\_search\_max\] to 0 in rippled.cfg file (RIPD-271) -- Add subscription to peer status changes (RIPD-579) - -**Improvements** - -- Improvements to ledger\_request response -- Improvements to validations proposal relaying (RIPD-1057) -- Improvements to consensus algorithm -- Ledger close time optimizations (RIPD-998, RIPD-791) -- Delete unfunded offers in predictable order - -**Development-Related Updates** - -- Require boost 1.57 -- Implement new coroutines (RIPD-1043) -- Force STAccount interface to 160-bit size (RIPD-994) -- Improve compile-time OpenSSL version check - -**Bug Fixes** - -- Fix handling of secp256r1 signatures (RIPD-1040) -- Fix websocket messages dispatching -- Fix pathfinding early response (RIPD-1064) -- Handle account\_objects empty response (RIPD-958) -- Fix delivered\_amount reporting for minor ledgers (RIPD-1051) -- Fix setting admin privileges on websocket -- Fix race conditions in account\_tx command (RIPD-1035) -- Fix to enforce no-ripple constraints - -**Assistance** - -For assistance, please contact **integration@ripple.com** - -**More information** - -For more information or assistance, the following resources will be of use: - -- [Ripple Developer Forums](https://ripple.com/forum/viewforum.php?f=2) -- [IRC](https://webchat.freenode.net/?channels=#ripple) - - - - ------------------------------------------------------------ - -## Version 0.30.0 - -rippled 0.30.0 has been released. The commit can be found on GitHub at: - -Prior to building, please confirm you have the correct source tree with the **git log** command. The first log entry should be the change setting the version: - - commit a8859b495b552fe1eb140771f0f2cb32d11d2ac2 - Author: Vinnie Falco - Date: Wed Oct 21 18:26:02 2015 -0700 - - Set version to 0.30.0 - -This release incorporates a number of important features, bugfixes and functional improvements. Please refer to the [Git commit history](https://github.com/ripple/rippled/commits/0.30.0) for more detailed information. - -**Release Overview** - -As part of Ripple Labs’ ongoing commitment toward protocol security, the rippled team would like to release rippled 0.30.0. - -**Toolchain support** - -The minimum supported version of GCC used to compile rippled is v4.8. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Ubuntu_versions_older_than_13.10_:_Install_gcc_4.8) if you have not upgraded already. - -While it may be possible to compile rippled on (virtual) machines with 4GB of RAM, we recommend compiling on (virtual) machines with 8GB of RAM or more. If your build machine has more than one CPU (**`grep '^processor' /proc/cpuinfo | wc -l`**), you can use them to assist in the build process by compiling with the command **`scons -j[number of CPUs - 1]`**. - -**New Features** - -- Honor markers in ledger\_data requests ([RIPD-1010](https://ripplelabs.atlassian.net/browse/RIPD-1010)). -- New Amendment - **TrustSetAuth** (Not currently enabled) Create zero balance trust lines with auth flag ([RIPD-1003](https://ripplelabs.atlassian.net/browse/RIPD-1003)): this allows a TrustSet transaction to create a trust line if the only thing being changed is setting the tfSetfAuth flag. -- Randomize the initial transaction execution order for closed ledgers based on the hash of the consensus set ([RIPD-961](https://ripplelabs.atlassian.net/browse/RIPD-961)). **Activates on October 27, 2015 at 11:00 AM PCT**. -- Differentiate path\_find response ([RIPD-1013](https://ripplelabs.atlassian.net/browse/RIPD-1013)). -- Convert all of an asset ([RIPD-655](https://ripplelabs.atlassian.net/browse/RIPD-655)). - -**Improvements** - -- SHAMap improvements. -- Upgrade SQLite from 3.8.8.2 to 3.8.11.1. -- Limit the number of offers that can be consumed during crossing ([RIPD-1026](https://ripplelabs.atlassian.net/browse/RIPD-1026)). -- Remove unfunded offers on tecOVERSIZE ([RIPD-1026](https://ripplelabs.atlassian.net/browse/RIPD-1026)). -- Improve transport security ([RIPD-1029](https://ripplelabs.atlassian.net/browse/RIPD-1029)): to take full advantage of the improved transport security, servers with a single, static public IP address should add it to their configuration file, as follows: - - [overlay] - public_ip= - -**Development-Related Updates** - -- Transitional support for gcc 5.2: to enable support define the environmental variable `RIPPLED_OLD_GCC_ABI`=1 -- Transitional support for C++ 14: to enable support define the environment variable `RIPPLED_USE_CPP_14`=1 -- Visual Studio 2015 support -- Updates to integration tests -- Add uptime to crawl data ([RIPD-997](https://ripplelabs.atlassian.net/browse/RIPD-997)) - -**Assistance** - -For assistance, please contact **integration@ripple.com** - -**More information** - -For more information or assistance, the following resources will be of use: - -- [Ripple Developer Forums](https://ripple.com/forum/viewforum.php?f=2) -- [IRC](https://webchat.freenode.net/?channels=#ripple) - - - - ------------------------------------------------------------ - -## Version 0.29.0 - -rippled 0.29.0 has been released. The commit can be found on GitHub at: - -Prior to building, please confirm you have the correct source tree with the **git log** command. The first log entry should be the change setting the version: - - commit 5964710f736e258c7892e8b848c48952a4c7856c - Author: Nik Bougalis - Date: Tue Aug 4 13:22:45 2015 -0700 - - Set version to 0.29.0 - -This release incorporates a number of important features, bugfixes and functional improvements. Please refer to the [Git commit history](https://github.com/ripple/rippled/commits/0.29.0) for more detailed information. - -**Release Overview** - -As part of Ripple Labs’ ongoing commitment toward protocol security, the rippled team would like to announce rippled release 0.29.0. - -**Toolchain support** - -The minimum supported version of GCC used to compile rippled is v4.8. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Ubuntu_versions_older_than_13.10_:_Install_gcc_4.8) if you have not upgraded already. - -While it may be possible to compile rippled on (virtual) machines with 4GB of RAM, we recommend compiling on (virtual) machines with 8GB of RAM or more. If your build machine has more than one CPU (**\`grep '^processor' /proc/cpuinfo | wc -l\`**), you can use them to assist in the build process by compiling with the command **scons -j\[number of CPUs - 1\]**. - -**New Features** - -- Subscription stream for validations ([RIPD-504](https://ripplelabs.atlassian.net/browse/RIPD-504)) - -**Deprecated features** - -- Disable Websocket ping timer - -**Bug Fixes** - -- Fix off-by one bug that overstates the account reserve during OfferCreate transaction. **Activates August 17, 2015**. -- Fix funded offer removal during Payment transaction ([RIPD-113](https://ripplelabs.atlassian.net/browse/RIPD-113)). **Activates August 17, 2015**. -- Fix display discrepancy in fee. - -**Improvements** - -- Add “quality” field to account\_offers API response: quality is defined as the exchange rate, the ratio taker\_pays divided by taker\_gets. -- Add [full\_reply](https://ripple.com/build/rippled-apis/#path-find-create) field to path\_find API response: full\_reply is defined as true/false value depending on the completed depth of pathfinding search ([RIPD-894](https://ripplelabs.atlassian.net/browse/RIPD-894)). -- Add [DeliverMin](https://ripple.com/build/transactions/#payment) transaction field ([RIPD-930](https://ripplelabs.atlassian.net/browse/RIPD-930)). **Activates August 17, 2015**. - -**Development-Related Updates** - -- Add uptime to crawl data ([RIPD-997](https://ripplelabs.atlassian.net/browse/RIPD-997)). -- Add IOUAmount and XRPAmount: these numeric types replace the monolithic functionality found in STAmount ([RIPD-976](https://ripplelabs.atlassian.net/browse/RIPD-976)). -- Log metadata differences on built ledger mismatch. -- Add enableTesting flag to applyTransactions. - -**Assistance** - -For assistance, please contact **integration@ripple.com** - -**More information** - -For more information or assistance, the following resources will be of use: - -- [Ripple Developer Forums](https://ripple.com/forum/viewforum.php?f=2) -- [IRC](https://webchat.freenode.net/?channels=#ripple) - - - - ------------------------------------------------------------ - -## Version 0.28.2 - -rippled 0.28.2 has been released. The commit can be found on GitHub at: - -Prior to building, please confirm you have the correct source tree with the **git log** command. The first log entry should be the change setting the version: - - commit 6374aad9bc94595e051a04e23580617bc1aaf300 - Author: Vinnie Falco - Date: Tue Jul 7 09:21:44 2015 -0700 - - Set version to 0.28.2 - -This release incorporates a number of important features, bugfixes and functional improvements. Please refer to the [Git commit history](https://github.com/ripple/rippled/commits/release) for more detailed information. - -**Release Overview** - -As part of Ripple Labs’ ongoing commitment toward protocol security, the rippled team would like to announce rippled release 0.28.2. **This release is necessary for compatibility with OpenSSL v.1.0.1n and later.** - -**Toolchain support** - -The minimum supported version of GCC used to compile rippled is v4.8. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Ubuntu_versions_older_than_13.10_:_Install_gcc_4.8) if you have not upgraded already. - -While it may be possible to compile rippled on (virtual) machines with 4GB of RAM, we recommend compiling on (virtual) machines with 8GB of RAM or more. If your build machine has more than one CPU (**\`grep '^processor' /proc/cpuinfo | wc -l\`**), you can use them to assist in the build process by compiling with the command **scons -j\[number of CPUs - 1\]**. - -The minimum supported version of Boost is v1.57.0. You **must** upgrade to this release or later to successfully compile this release of rippled. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Install_Boost) if you have not upgraded already. - -**rippled.cfg Updates** - -For \[ips\] stanza, a port must be specified for each listed IP address with the space between IP address and port, ex.: `r.ripple.com` `51235` ([RIPD-893](https://ripplelabs.atlassian.net/browse/RIPD-893)) - -**New Features** - -- New API: [gateway\_balances](https://ripple.com/build/rippled-apis/#gateway-balances) to get a gateway's hot wallet balances and total obligations. - -**Deprecated features** - -- Removed temp\_db ([RIPD-887](https://ripplelabs.atlassian.net/browse/RIPD-887)) - -**Improvements** - -- Improve peer send queue management -- Support larger EDH keys -- More robust call to get the valid ledger index -- Performance improvements to transactions application to open ledger - -**Development-Related Updates** - -- New Env transaction testing framework for unit testing -- Fix MSVC link -- C++ 14 readiness - -**Assistance** - -For assistance, please contact **integration@ripple.com** - -**More information** - -For more information or assistance, the following resources will be of use: - -- [Ripple Developer Forums](https://ripple.com/forum/viewforum.php?f=2) -- [IRC](https://webchat.freenode.net/?channels=#ripple) - - - - ------------------------------------------------------------ - -## Version 0.28.1 - -rippled 0.28.1 has been released. The commit can be found on GitHub at: - -Prior to building, please confirm you have the correct source tree with the **git log** command. The first log entry should be the change setting the version: - - commit 399c43cae6e90a428e9ce6a988123972b0f03c99 - Author: Miguel Portilla - Date: Wed May 20 13:30:54 2015 -0400 - - Set version to 0.28.1 - -This release incorporates a number of important features, bugfixes and functional improvements. Please refer to the [Git commit history](https://github.com/ripple/rippled/commits/0.28.1) for more detailed information. - -**Toolchain support** - -The minimum supported version of GCC used to compile rippled is v4.8. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Ubuntu_versions_older_than_13.10_:_Install_gcc_4.8) if you have not upgraded already. - -While it may be possible to compile rippled on (virtual) machines with 4GB of RAM, we recommend compiling on (virtual) machines with 8GB of RAM or more. If your build machine has more than one CPU (**\`grep '^processor' /proc/cpuinfo | wc -l\`**), you can use them to assist in the build process by compiling with the command **scons -j\[number of CPUs - 1\]**. - -The minimum supported version of Boost is v1.57.0. You **must** upgrade to this release or later to successfully compile this release of rippled. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Install_Boost) if you have not upgraded already. - -**New Features** - -- Filtering for Account Objects ([RIPD-868](https://ripplelabs.atlassian.net/browse/RIPD-868)). -- Track rippled server peers latency ([RIPD-879](https://ripplelabs.atlassian.net/browse/RIPD-879)). - -**Bug fixes** - -- Expedite zero flow handling for offers -- Fix offer crossing when funds are the limiting factor - -**Deprecated features** - -- Wallet\_accounts and generator maps ([RIPD-804](https://ripplelabs.atlassian.net/browse/RIPD-804)) - -**Improvements** - -- Control ledger query depth based on peers latency -- Improvements to ledger history fetches -- Improve RPC ledger synchronization requirements ([RIPD-27](https://ripplelabs.atlassian.net/browse/RIPD-27), [RIPD-840](https://ripplelabs.atlassian.net/browse/RIPD-840)) -- Eliminate need for ledger in delivered\_amount calculation ([RIPD-860](https://ripplelabs.atlassian.net/browse/RIPD-860)) -- Improvements to JSON parsing - -**Development-Related Updates** - -- Add historical ledger fetches per minute to get\_counts -- Compute validated ledger age from signing time -- Remove unused database table ([RIPD-755](https://ripplelabs.atlassian.net/browse/RIPD-755)) - -**Assistance** - -For assistance, please contact **integration@ripple.com** - -**More information** - -For more information or assistance, the following resources will be of use: - -- [Ripple Developer Forums](https://ripple.com/forum/viewforum.php?f=2) -- [IRC](https://webchat.freenode.net/?channels=#ripple) - - - - ------------------------------------------------------------ - -## Version 0.28.0 - -rippled 0.28.0 has been released. The commit can be found on GitHub at: - -Prior to building, please confirm you have the correct source tree with the **git log** command. The first log entry should be the change setting the version: - - commit 7efd0ab0d6ef017331a0e214a3053893c88f38a9 - Author: Vinnie Falco - Date: Fri Apr 24 18:57:36 2015 -0700 - - Set version to 0.28.0 - -This release incorporates a number of important features, bugfixes and functional improvements. Please refer to the [Git commit history](https://github.com/ripple/rippled/commits/0.28.0) for more detailed information. - -**Release Overview** - -As part of Ripple Labs’ ongoing commitment toward improving the protocol, the rippled team is excited to announce **autobridging** — a feature that allows XRP to serve as a bridge currency. Autobridging enhances utility and has the potential to expose more of the network to liquidity and improve prices. For more information please refer to the [autobridging blog post](https://ripple.com/uncategorized/introducing-offer-autobridging/). - -**Toolchain support** - -The minimum supported version of GCC used to compile rippled is v4.8. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Ubuntu_versions_older_than_13.10_:_Install_gcc_4.8) if you have not upgraded already. - -While it may be possible to compile rippled on (virtual) machines with 4GB of RAM, we recommend compiling on (virtual) machines with 8GB of RAM or more. If your build machine has more than one CPU (**\`grep '^processor' /proc/cpuinfo | wc -l\`**), you can use them to assist in the build process by compiling with the command **scons -j\[number of CPUs - 1\]**. - -The minimum supported version of Boost is v1.57.0. You **must** upgrade to this release or later to successfully compile this release of rippled. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Install_Boost) if you have not upgraded already. - -**Important rippled.cfg update** - -With rippled version 0.28, the rippled.cfg file must be changed according to these instructions: - -- Change any entries that say - -`admin` `=` `allow` to `admin` `=` - -- For most installations, 127.0.0.1 will preserve current behavior. 0.0.0.0 may be specified to indicate "any IP" but cannot be combined with other IP addresses. Use of 0.0.0.0 may introduce severe security risks and is not recommended. See docs/rippled-example.cfg for more information. - -**More Strict Requirements on MemoType** - -The requirements on the contents of the MemoType field, if present, are more strict than the previous version. Transactions that can successfully be submitted to 0.27.4 and earlier may fail in 0.28.0. For details, please refer to [updated memo documentation](https://ripple.com/build/transactions/#memos) for details. Partners should check their implementation to make sure that their MemoType follows the new rules. - -**New Features** - -- Autobridging implementation ([RIPD-423](https://ripplelabs.atlassian.net/browse/RIPD-423)). **This feature will be turned on May 12, 2015**. -- Combine history\_ledger\_index and online\_delete settings in rippled.cfg ([RIPD-774](https://ripplelabs.atlassian.net/browse/RIPD-774)). -- Claim a fee when a required destination tag is not specified ([RIPD-574](https://ripplelabs.atlassian.net/browse/RIPD-574)). -- Require the master key when disabling the use of the master key or when enabling 'no freeze' ([RIPD-666](https://ripplelabs.atlassian.net/browse/RIPD-666)). -- Change the port setting admin to accept allowable admin IP addresses ([RIPD-820](https://ripplelabs.atlassian.net/browse/RIPD-820)): - - rpc\_admin\_allow has been removed. - - Comma-separated list of IP addresses that are allowed administrative privileges (subject to username & password authentication if configured). - - 127.0.0.1 is no longer a default admin IP. - - 0.0.0.0 may be specified to indicate "any IP" but cannot be combined with other MIP addresses. Use of 0.0.0.0 may introduce severe security risks and is not recommended. -- Enable Amendments from config file or static data ([RIPD-746](https://ripplelabs.atlassian.net/browse/RIPD-746)). - -**Bug fixes** - -- Fix payment engine handling of offer ⇔ account ⇔ offer cases ([RIPD-639](https://ripplelabs.atlassian.net/browse/RIPD-639)). **This fix will take effect on May 12, 2015**. -- Fix specified destination issuer in pathfinding ([RIPD-812](https://ripplelabs.atlassian.net/browse/RIPD-812)). -- Only report delivered\_amount for executed payments ([RIPD-827](https://ripplelabs.atlassian.net/browse/RIPD-827)). -- Return a validated ledger if there is one ([RIPD-814](https://ripplelabs.atlassian.net/browse/RIPD-814)). -- Refund owner's ticket reserve when a ticket is canceled ([RIPD-855](https://ripplelabs.atlassian.net/browse/RIPD-855)). -- Return descriptive error from account\_currencies RPC ([RIPD-806](https://ripplelabs.atlassian.net/browse/RIPD-806)). -- Fix transaction enumeration in account\_tx API ([RIPD-734](https://ripplelabs.atlassian.net/browse/RIPD-734)). -- Fix inconsistent ledger\_current return ([RIPD-669](https://ripplelabs.atlassian.net/browse/RIPD-669)). -- Fix flags --rpc\_ip and --rpc\_port ([RIPD-679](https://ripplelabs.atlassian.net/browse/RIPD-679)). -- Skip inefficient SQL query ([RIPD-870](https://ripplelabs.atlassian.net/browse/RIPD-870)) - -**Deprecated features** - -- Remove support for deprecated PreviousTxnID field ([RIPD-710](https://ripplelabs.atlassian.net/browse/RIPD-710)). **This will take effect on May 12, 2015**. -- Eliminate temREDUNDANT\_SEND\_MAX ([RIPD-690](https://ripplelabs.atlassian.net/browse/RIPD-690)). -- Remove WalletAdd ([RIPD-725](https://ripplelabs.atlassian.net/browse/RIPD-725)). -- Remove SMS support. - -**Improvements** - -- Improvements to peer communications. -- Reduce master lock for client requests. -- Update SQLite to 3.8.8.2. -- Require Boost 1.57. -- Improvements to Universal Port ([RIPD-687](https://ripplelabs.atlassian.net/browse/RIPD-687)). -- Constrain valid inputs for memo fields ([RIPD-712](https://ripplelabs.atlassian.net/browse/RIPD-712)). -- Binary option for ledger command ([RIPD-692](https://ripplelabs.atlassian.net/browse/RIPD-692)). -- Optimize transaction checks ([RIPD-751](https://ripplelabs.atlassian.net/browse/RIPD-751)). - -**Development-Related Updates** - -- Add RPC metrics ([RIPD-705](https://ripplelabs.atlassian.net/browse/RIPD-705)). -- Track and report peer load. -- Builds/Test.py will build and test by one or more scons targets. -- Support a --noserver command line option in tests: -- Run npm/integration tests without launching rippled, using a running instance of rippled (possibly in a debugger) instead. -- Works for npm test and mocha. -- Display human readable SSL error codes. -- Better transaction analysis ([RIPD-755](https://ripplelabs.atlassian.net/browse/RIPD-755)). - -**Assistance** - -For assistance, please contact **integration@ripple.com** - -**More information** - -For more information or assistance, the following resources will be of use: - -- [Ripple Developer Forums](https://ripple.com/forum/viewforum.php?f=2) -- [IRC](https://webchat.freenode.net/?channels=#ripple) - - - - ------------------------------------------------------------ - -## Version 0.27.4 - -rippled 0.27.4 has been released. The commit can be found on GitHub at: - -Prior to building, please confirm you have the correct source tree with the **git log** command. The first log entry should be the change setting the version: - - commit 92812fe7239ffa3ba91649b2ece1e892b866ec2a - Author: Nik Bougalis - Date: Wed Mar 11 11:26:44 2015 -0700 - - Set version to 0.27.4 - -This release includes one new feature. Please refer to the [Git commit history](https://github.com/ripple/rippled/commits/0.27.4) for more detailed information. - -**Toolchain support** - -The minimum supported version of GCC used to compile rippled is v4.8. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Ubuntu_versions_older_than_13.10_:_Install_gcc_4.8) if you have not upgraded already. - -While it may be possible to compile rippled on (virtual) machines with 4GB of RAM, we recommend compiling on (virtual) machines with 8GB of RAM or more. If your build machine has more than one CPU (**\`grep '^processor' /proc/cpuinfo | wc -l\`**), you can use them to assist in the build process by compiling with the command **scons -j\[number of CPUs - 1\]**. - -The minimum supported version of Boost is v1.57.0. You **must** upgrade to this release or later to successfully compile this release of rippled. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Install_Boost) if you have not upgraded already. - -**Bug Fixes** - -- Limit passes in the payment engine - -**Assistance** - -For assistance, please contact **integration@ripple.com** - -**More information** - -For more information or assistance, the following resources will be of use: - -- [Ripple Developer Forums](https://ripple.com/forum/viewforum.php?f=2) -- [IRC](https://webchat.freenode.net/?channels=#ripple) - - - - ------------------------------------------------------------ - -## Version 0.27.3-sp2 - -rippled 0.27.3-sp2 has been released. The commit can be found on GitHub at: - -Prior to building, please confirm you have the correct source tree with the **git log** command. The first log entry should be the change setting the version: - - commit f999839e599e131ed624330ad0ce85bb995f02d3 - Author: Nik Bougalis - Date: Thu Mar 12 13:37:47 2015 -0700 - - Set version to 0.27.3-sp2 - -This release includes one new feature. Please refer to the [Git commit history](https://github.com/ripple/rippled/commits/0.27.3-sp2) for more detailed information. - -**Toolchain support** - -The minimum supported version of GCC used to compile rippled is v4.8. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Ubuntu_versions_older_than_13.10_:_Install_gcc_4.8) if you have not upgraded already. - -While it may be possible to compile rippled on (virtual) machines with 4GB of RAM, we recommend compiling on (virtual) machines with 8GB of RAM or more. If your build machine has more than one CPU (**\`grep '^processor' /proc/cpuinfo | wc -l\`**), you can use them to assist in the build process by compiling with the command **scons -j\[number of CPUs - 1\]**. - -The minimum supported version of Boost is v1.57.0. You **must** upgrade to this release or later to successfully compile this release of rippled. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Install_Boost) if you have not upgraded already. - -**New Features** - -- Add noripple\_check RPC command: this command tells gateways what they need to do to set "Default Ripple" account flag and fix any trust lines created before the flag was set. - -**Assistance** - -For assistance, please contact **integration@ripple.com** - -**More information** - -For more information or assistance, the following resources will be of use: - -- [Ripple Developer Forums](https://ripple.com/forum/viewforum.php?f=2) -- [IRC](https://webchat.freenode.net/?channels=#ripple) - - - - ------------------------------------------------------------ - -## Version 0.27.3-sp1 - -rippled 0.27.3-sp1 has been released. The commit can be found on GitHub at: - -Prior to building, please confirm you have the correct source tree with the **git log** command. The first log entry should be the change setting the version: - - commit 232693419a2c9a8276a0fae991f688f6f01a3add - Author: Nik Bougalis - Date: Wed Mar 11 10:26:39 2015 -0700 - - Set version to 0.27.3-sp1 - -This release includes one new feature. Please refer to the [Git commit history](https://github.com/ripple/rippled/commits/0.27.3-sp1) for more detailed information. - -**Toolchain support** - -The minimum supported version of GCC used to compile rippled is v4.8. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Ubuntu_versions_older_than_13.10_:_Install_gcc_4.8) if you have not upgraded already. - -While it may be possible to compile rippled on (virtual) machines with 4GB of RAM, we recommend compiling on (virtual) machines with 8GB of RAM or more. If your build machine has more than one CPU (**\`grep '^processor' /proc/cpuinfo | wc -l\`**), you can use them to assist in the build process by compiling with the command **scons -j\[number of CPUs - 1\]**. - -The minimum supported version of Boost is v1.57.0. You **must** upgrade to this release or later to successfully compile this release of rippled. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Install_Boost) if you have not upgraded already. - -**New Features** - -- Add "Default Ripple" account flag - -**Assistance** - -For assistance, please contact **integration@ripple.com** - -**More information** - -For more information or assistance, the following resources will be of use: - -- [Ripple Developer Forums](https://ripple.com/forum/viewforum.php?f=2) -- [IRC](https://webchat.freenode.net/?channels=#ripple) - - ------------------------------------------------------------ - -## Version 0.27.3 - -rippled 0.27.3 has been released. The commit can be found on GitHub at: - -Prior to building, please confirm you have the correct source tree with the **git log** command. The first log entry should be the change setting the version: - - commit 70c2854f7c8a28801a7ebc81dd62bf0d068188f0 - Author: Nik Bougalis - Date: Tue Mar 10 14:06:33 2015 -0700 - - Set version to 0.27.3 - -This release includes one new feature. Please refer to the [Git commit history](https://github.com/ripple/rippled/commits/0.27.3) for more detailed information. - -**Toolchain support** - -The minimum supported version of GCC used to compile rippled is v4.8. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Ubuntu_versions_older_than_13.10_:_Install_gcc_4.8) if you have not upgraded already. - -While it may be possible to compile rippled on (virtual) machines with 4GB of RAM, we recommend compiling on (virtual) machines with 8GB of RAM or more. If your build machine has more than one CPU (**\`grep '^processor' /proc/cpuinfo | wc -l\`**), you can use them to assist in the build process by compiling with the command **scons -j\[number of CPUs - 1\]**. - -The minimum supported version of Boost is v1.57.0. You **must** upgrade to this release or later to successfully compile this release of rippled. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Install_Boost) if you have not upgraded already. - -**New Features** - -- Add "Default Ripple" account flag - -**Assistance** - -For assistance, please contact **integration@ripple.com** - -**More information** - -For more information or assistance, the following resources will be of use: - -- [Ripple Developer Forums](https://ripple.com/forum/viewforum.php?f=2) -- [IRC](https://webchat.freenode.net/?channels=#ripple) - - ------------------------------------------------------------ - -## Version 0.27.2 - -rippled 0.27.2 has been released. The commit can be found on GitHub at: - -Prior to building, please confirm you have the correct source tree with the **git log** command. The first log entry should be the change setting the version: - - commit 9cc8eec773e8afc9c12a6aab4982deda80495cf1 - Author: Nik Bougalis - Date: Sun Mar 1 14:56:44 2015 -0800 - - Set version to 0.27.2 - -This release incorporates a number of important bugfixes and functional improvements. Please refer to the [Git commit history](https://github.com/ripple/rippled/commits/0.27.2) for more detailed information. - -**Toolchain support** - -The minimum supported version of GCC used to compile rippled is v4.8. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Ubuntu_versions_older_than_13.10_:_Install_gcc_4.8) if you have not upgraded already. - -While it may be possible to compile rippled on (virtual) machines with 4GB of RAM, we recommend compiling on (virtual) machines with 8GB of RAM or more. If your build machine has more than one CPU (**\`grep '^processor' /proc/cpuinfo | wc -l\`**), you can use them to assist in the build process by compiling with the command **scons -j\[number of CPUs - 1\]**. - -The minimum supported version of Boost is v1.57.0. You **must** upgrade to this release or later to successfully compile this release of rippled. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Install_Boost) if you have not upgraded already. - -**New Features** - -- NuDB backend option: high performance key/value database optimized for rippled (set “type=nudb” in .cfg). - - Either import RockdDB to NuDB using import tool, or - - Start fresh with NuDB but delete SQLite databases if rippled ran previously with RocksDB: - - rm [database_path]/transaction.* [database_path]/ledger.* - -**Bug Fixes** - -- Fix offer quality bug - -**Deprecated** - -- HyperLevelDB, LevelDB, and SQLlite backend options. Use RocksDB for spinning drives and NuDB for SSDs backend options. - -**Assistance** - -For assistance, please contact **integration@ripple.com** - -**More information** - -For more information or assistance, the following resources will be of use: - -- [Ripple Developer Forums](https://ripple.com/forum/viewforum.php?f=2) -- [IRC](https://webchat.freenode.net/?channels=#ripple) - - ------------------------------------------------------------ - -## Version 0.27.1 - -rippled 0.27.1 has been released. The commit can be found on GitHub at: - -Prior to building, please confirm you have the correct source tree with the **git log** command. The first log entry should be the change setting the version: - - commit 95973ba3e8b0bd28eeaa034da8b806faaf498d8a - Author: Vinnie Falco - Date: Tue Feb 24 13:31:13 2015 -0800 - - Set version to 0.27.1 - -This release incorporates a number of important bugfixes and functional improvements. Please refer to the [Git commit history](https://github.com/ripple/rippled/commits/0.27.1) for more detailed information. - -**Toolchain support** - -The minimum supported version of GCC used to compile rippled is v4.8. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Ubuntu_versions_older_than_13.10_:_Install_gcc_4.8) if you have not upgraded already. - -While it may be possible to compile rippled on (virtual) machines with 4GB of RAM, we recommend compiling on (virtual) machines with 8GB of RAM or more. If your build machine has more than one CPU (**\`grep '^processor' /proc/cpuinfo | wc -l\`**), you can use them to assist in the build process by compiling with the command **scons -j\[number of CPUs - 1\]**. - -The minimum supported version of Boost is v1.57.0. You **must** upgrade to this release or later to successfully compile this release of rippled. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Install_Boost) if you have not upgraded already. - -**New Features** - -- RocksDB to NuDB import tool ([RIPD-781](https://ripplelabs.atlassian.net/browse/RIPD-781), [RIPD-785](https://ripplelabs.atlassian.net/browse/RIPD-785)): custom tool specifically designed for very fast import of RocksDB nodestore databases into NuDB - -**Bug Fixes** - -- Fix streambuf bug - -**Improvements** - -- Update RocksDB backend settings -- NuDB improvements: - - Limit size of mempool ([RIPD-787](https://ripplelabs.atlassian.net/browse/RIPD-787)) - - Performance improvements ([RIPD-793](https://ripplelabs.atlassian.net/browse/RIPD-793), [RIPD-796](https://ripplelabs.atlassian.net/browse/RIPD-796)): changes in Nudb to improve speed, reduce database size, and enhance correctness. The most significant change is to store hashes rather than entire keys in the key file. The output of the hash function is reduced to 48 bits, and stored directly in buckets. - -**Experimental** - -- Add /crawl cgi request feature to peer protocol ([RIPD-729](https://ripplelabs.atlassian.net/browse/RIPD-729)): adds support for a cgi /crawl request, issued over HTTPS to the configured peer protocol port. The response to the request is a JSON object containing the node public key, type, and IP address of each directly connected neighbor. The IP address is suppressed unless the neighbor has requested its address to be revealed by adding "Crawl: public" to its HTTP headers. This field is currently set by the peer\_private option in the rippled.cfg file. - -**Assistance** - -For assistance, please contact **integration@ripple.com** - -**More information** - -For more information or assistance, the following resources will be of use: - -- [Ripple Developer Forums](https://ripple.com/forum/viewforum.php?f=2) -- [IRC](https://webchat.freenode.net/?channels=#ripple) - - ------------------------------------------------------------ - -## Version 0.27.0 - -rippled 0.27.0 has been released. The commit can be found on GitHub at: - -Prior to building, please confirm you have the correct source tree with the **git log** command. The first log entry should be the change setting the version: - - commit c6c8e5d70c6fbde02cd946135a061aa77744396f - Author: Vinnie Falco - Date: Mon Jan 26 10:56:11 2015 -0800 - - Set version to 0.27.0 - -This release incorporates a number of important bugfixes and functional improvements. Please refer to the [Git commit history](https://github.com/ripple/rippled/commits/0.27.0) for more detailed information. - -**Release Overview** - -The rippled team is proud to release rippled 0.27.0. This new version includes many exciting features that will appeal to our users. The team continues to work on stability, scalability, and performance. - -The first feature is Online Delete. This feature allows rippled to maintain it’s database of previous ledgers within a fixed amount of disk space. It does this while allowing rippled to stay online and maintain an administrator specify minimum number of ledgers. This means administrators with limited disk space will no longer need to manage disk space by periodically manually removing the database. Also, with the previously existing backend databases performance would gradually degrade as the database grew in size. In particular, rippled would perform poorly whenever the backend database performed ever growing compaction operations. By limiting rippled to less history, compaction is less resource intensive and systems with less disk performance can now run rippled. - -Additionally, we are very excited to include Universal Port. This feature allows rippled's listening port to handshake in multiple protocols. For example, a single listening port can be configured to receive incoming peer connections, incoming RPC commands over HTTP, and incoming RPC commands over HTTPS at the same time. Or, a single port can receive both Websockets and Secure Websockets clients at the same. - -Finally, a new, experimental backend database, NuDB, has been added. This database was developed by Ripple Labs to take advantage of rippled’s specific data usage profile and performs much better than previous databases. Significantly, this database does not degrade in performance as the database grows. Very excitingly, this database works on OS X and Windows. This allows rippled to use these platforms for the first time. - -**Toolchain support** - -The minimum supported version of GCC used to compile rippled is v4.8. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Ubuntu_versions_older_than_13.10_:_Install_gcc_4.8) if you have not upgraded already. - -While it may be possible to compile rippled on (virtual) machines with 4GB of RAM, we recommend compiling on (virtual) machines with 8GB of RAM or more. If your build machine has more than one CPU (**\`grep '^processor' /proc/cpuinfo | wc -l\`**), you can use them to assist in the build process by compiling with the command **scons -j\[number of CPUs - 1\]**. - -The minimum supported version of Boost is v1.57.0. You **must** upgrade to this release or later to successfully compile this release of rippled. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Install_Boost) if you have not upgraded already. - -**Important rippled.cfg Update** - -**The format of the configuration file has changed. If upgrading from a previous version of rippled, please see the migration instructions below.** - -**New Features** - -- SHAMapStore Online Delete ([RIPD-415](https://ripplelabs.atlassian.net/browse/RIPD-415)): Makes rippled configurable to support deletion of all data in its key-value store (nodestore) and ledger and transaction SQLite databases based on validated ledger sequence numbers. See doc/rippled-example.cfg for configuration setup. -- [Universal Port](https://forum.ripple.com/viewtopic.php?f=2&t=8313&p=57969). See necessary config changes below. -- Config "ledger\_history\_index" option ([RIPD-559](https://ripplelabs.atlassian.net/browse/RIPD-559)) - -**Bug Fixes** - -- Fix pathfinding with multiple issuers for one currency ([RIPD-618](https://ripplelabs.atlassian.net/browse/RIPD-618)) -- Fix account\_lines, account\_offers and book\_offers result ([RIPD-682](https://ripplelabs.atlassian.net/browse/RIPD-682)) -- Fix pathfinding bugs ([RIPD-735](https://ripplelabs.atlassian.net/browse/RIPD-735)) -- Fix RPC subscribe with multiple books ([RIPD-77](https://ripplelabs.atlassian.net/browse/RIPD-77)) -- Fix account\_tx API - -**Improvements** - -- Improve the human-readable description of the tesSUCCESS code -- Add 'delivered\_amount' to Transaction JSON ([RIPD-643](https://ripplelabs.atlassian.net/browse/RIPD-643)): The synthetic field 'delivered\_amount' can be used to determine the exact amount delivered by a Payment without having to check the DeliveredAmount field, if present, or the Amount field otherwise. - -**Development-Related Updates** - -- HTTP Handshaking for Peers on Universal Port ([RIPD-446](https://ripplelabs.atlassian.net/browse/RIPD-446)) -- Use asio signal handling in Application ([RIPD-140](https://ripplelabs.atlassian.net/browse/RIPD-140)) -- Build dependency on Boost 1.57.0 -- Support a "no\_server" flag in test config -- API for improved Unit Testing ([RIPD-432](https://ripplelabs.atlassian.net/browse/RIPD-432)) -- Option to specify rippled path on command line (--rippled=\) - -**Experimental** - -- NuDB backend option: high performance key/value database optimized for rippled (set “type=nudb” in .cfg) - -**Migration Instructions** - -With rippled version 0.27.0, the rippled.cfg file must be changed according to these instructions: - -- Add new stanza - `[server]`. This section will contain a list of port names and key/value pairs. A port name must start with a letter and contain only letters and numbers. The name is not case-sensitive. For each name in this list, rippled will look for a configuration file section with the same name and use it to create a listening port. To simplify migration, you can use port names from your previous version of rippled.cfg (see Section 1. Server for detailed explanation in doc/rippled-example.cfg). For example: - - [server] - rpc_port - peer_port - websocket_port - ssl_key = - ssl_cert = - ssl_chain = - -- For each port name in `[server]` stanza, add separate stanzas. For example: - - [rpc_port] - port = - ip = - admin = allow - protocol = https - - [peer_port] - port = - ip = - protocol = peer - - [websocket_port] - port = - ip = - admin = allow - protocol = wss - -- Remove current `[rpc_port],` `[rpc_ip],` `[rpc_allow_remote],` `[rpc_ssl_key],` `[rpc_ssl_cert],` `and` `[rpc_ssl_chain],` `[peer_port],` `[peer_ip],` `[websocket_port],` `[websocket_ip]` settings from rippled.cfg - -- If you allow untrusted websocket connections to your rippled, add `[websocket_public_port]` stanza under `[server]` section and replace websocket public settings with `[websocket_public_port]` section: - - [websocket_public_port] - port = - ip = - protocol = ws ← make sure this is ws, not wss` - -- Remove `[websocket_public_port],` `[websocket_public_ip],` `[websocket_ssl_key],` `[websocket_ssl_cert],` `[websocket_ssl_chain]` settings from rippled.cfg -- Disable `[ssl_verify]` section by setting it to 0 -- Migrate the remaining configurations without changes. To enable online delete feature, check Section 6. Database in doc/rippled-example.cfg - -**Integration Notes** - -With this release, integrators should deprecate the "DeliveredAmount" field in favor of "delivered\_amount." - -**For Transactions That Occurred Before January 20, 2014:** - -- If amount actually delivered is different than the transactions “Amount” field - - "delivered\_amount" will show as unavailable indicating a developer should use caution when processing this payment. - - Example: A partial payment transaction (tfPartialPayment). -- Otherwise - - "delivered\_amount" will show the correct destination balance change. - -**For Transactions That Occur After January 20, 2014:** - -- If amount actually delivered is different than the transactions “Amount” field - - A "delivered\_amount" field will determine the destination amount change - - Example: A partial payment transaction (tfPartialPayment). -- Otherwise - - "delivered\_amount" will show the correct destination balance change. - -**Assistance** - -For assistance, please contact **integration@ripple.com** - -**More information** - -For more information or assistance, the following resources will be of use: - -- [Ripple Developer Forums](https://ripple.com/forum/viewforum.php?f=2) -- [IRC](https://webchat.freenode.net/?channels=#ripple) - - ------------------------------------------------------------ - -## Version 0.26.4 - -rippled 0.26.4 has been released. The repository tag is *0.26.4* and can be found on GitHub at: - -Prior to building, please confirm you have the correct source tree with the **git log** command. The first log entry should be the change setting the version: - - commit 05a04aa80192452475888479c84ff4b9b54e6ae7 - Author: Vinnie Falco - Date: Mon Nov 3 16:53:37 2014 -0800 - - Set version to 0.26.4 - -This release incorporates a number of important bugfixes and functional improvements. Please refer to the [Git commit history](https://github.com/ripple/rippled/commits/0.26.4) for more detailed information. - -**Toolchain support** - -The minimum supported version of GCC used to compile rippled is v4.8. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Ubuntu_versions_older_than_13.10_:_Install_gcc_4.8) if you have not upgraded already. - -While it may be possible to compile rippled on (virtual) machines with 4GB of RAM, we recommend compiling on (virtual) machines with 8GB of RAM or more. If your build machine has more than one CPU (**\`grep '^processor' /proc/cpuinfo | wc -l\`**), you can use them to assist in the build process by compiling with the command **scons -j\[number of CPUs - 1\]**. - -The minimum supported version of Boost is v1.55.0. You **must** upgrade to this release or later to successfully compile this release of rippled. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Install_Boost) if you have not upgraded already. - -**Important JSON-RPC Update** - -With rippled version 0.26.4, the [rippled.cfg](https://github.com/ripple/rippled/blob/0.26.4/doc/rippled-example.cfg) file must set the ssl\_verify property to 0. Without this update, JSON-RPC API calls may not work. - -**New Features** - -- Rocksdb v. 3.5.1 -- SQLite v. 3.8.7 -- Disable SSLv3 -- Add counters to track ledger read and write activities -- Use trusted validators median fee when determining transaction fee -- Add --quorum argument for server start ([RIPD-563](https://ripplelabs.atlassian.net/browse/RIPD-563)) -- Add account\_offers paging ([RIPD-344](https://ripplelabs.atlassian.net/browse/RIPD-344)) -- Add account\_lines paging ([RIPD-343](https://ripplelabs.atlassian.net/browse/RIPD-343)) -- Ability to configure network fee in rippled.cfg file ([RIPD-564](https://ripplelabs.atlassian.net/browse/RIPD-564)) - -**Bug Fixes** - -- Fix OS X version parsing/error related to OS X 10.10 update -- Fix incorrect address in connectivity check report -- Fix page sizes for ledger\_data ([RIPD-249](https://ripplelabs.atlassian.net/browse/RIPD-249)) -- Make log partitions case-insensitive in rippled.cfg - -**Improvements** - -- Performance - - Ledger performance improvements for storage and traversal ([RIPD-434](https://ripplelabs.atlassian.net/browse/RIPD-434)) - - Improve client performance for JSON responses ([RIPD-439](https://ripplelabs.atlassian.net/browse/RIPD-439)) -- Other - - Remove PROXY handshake feature - - Change to rippled.cfg to support sections containing both key/value pairs and a list of values - - Return descriptive error message for memo validation ([RIPD-591](https://ripplelabs.atlassian.net/browse/RIPD-591)) - - Changes to enforce JSON-RPC 2.0 error format - - Optimize account\_lines and account\_offers ([RIPD-587](https://ripplelabs.atlassian.net/browse/RIPD-587)) - - Improve fee setting logic ([RIPD-614](https://ripplelabs.atlassian.net/browse/RIPD-614)) - - Improve transaction security - - Config improvements - - Improve path filtering ([RIPD-561](https://ripplelabs.atlassian.net/browse/RIPD-561)) - - Logging to distinguish Byzantine failure from tx bug ([RIPD-523](https://ripplelabs.atlassian.net/browse/RIPD-523)) - -**Experimental** - -- Add "deferred" flag to transaction relay message (required for future code that will relay deferred transactions) -- Refactor STParsedJSON to parse an object or array (required for multisign implementation) ([RIPD-480](https://ripplelabs.atlassian.net/browse/RIPD-480)) - -**Development-Related Updates** - -- Changes to DatabaseReader to read ledger numbers from database -- Improvements to SConstruct - -**More information** - -For more information or assistance, the following resources will be of use: - -- [Ripple Developer Forums](https://ripple.com/forum/viewforum.php?f=2) -- [IRC](https://webchat.freenode.net/?channels=#ripple) - - ------------------------------------------------------------ - -## Version 0.26.3-sp1 - -rippled 0.26.3-sp1 has been released. The repository tag is *0.26.3-sp1* and can be found on GitHub at: - -Prior to building, please confirm you have the correct source tree with the **git log** command. The first log entry should be the change setting the version: - - commit 2ad6f0a65e248b4f614d38d199a9d5d02f5aaed8 - Author: Vinnie Falco - Date: Fri Sep 12 15:22:54 2014 -0700 - - Set version to 0.26.3-sp1 - -This release incorporates a number of important bugfixes and functional improvements. Please refer to the [Git commit history](https://github.com/ripple/rippled/commits/0.26.3-sp1) for more detailed information. - -**Toolchain support** - -The minimum supported version of GCC used to compile rippled is v4.8. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Ubuntu_versions_older_than_13.10_:_Install_gcc_4.8) if you have not upgraded already. - -While it may be possible to compile rippled on (virtual) machines with 4GB of RAM, we recommend compiling on (virtual) machines with 8GB of RAM or more. If your build machine has more than one CPU (**\`grep '^processor' /proc/cpuinfo | wc -l\`**), you can use them to assist in the build process by compiling with the command **scons -j\[number of CPUs - 1\]**. - -The minimum supported version of Boost is v1.55.0. You **must** upgrade to this release or later to successfully compile this release of rippled. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Install_Boost) if you have not upgraded already. - -**New Features** - -- New command to display HTTP/S-RPC sessions metrics ([RIPD-533](https://ripplelabs.atlassian.net/browse/RIPD-533)) - -**Bug Fixes** - -- Improved handling of HTTP/S-RPC sessions ([RIPD-489](https://ripplelabs.atlassian.net/browse/RIPD-489)) -- Fix unit tests for Windows. -- Fix integer overflows in JSON parser. - -**Improvements** - -- Improve processing of trust lines during pathfinding. - -**Experimental Features** - -- Added a command line utility called LedgerTool for retrieving and processing ledger blocks from the Ripple network. - -**Development-Related Updates** - -- HTTP message and parser improvements. - - Streambuf wrapper supports rvalue move. - - Message class holds a complete HTTP message. - - Body class holds the HTTP content body. - - Headers class holds RFC-compliant HTTP headers. - - Basic\_parser provides class interface to joyent's http-parser. - - Parser class parses into a message object. - - Remove unused http get client free function. - - Unit test for parsing malformed messages. -- Add enable\_if\_lvalue. -- Updates to includes and scons. -- Additional ledger.history.mismatch insight statistic. -- Convert rvalue to an lvalue. ([RIPD-494](https://ripplelabs.atlassian.net/browse/RIPD-494)) -- Enable heap profiling with jemalloc. -- Add aged containers to Validators module. ([RIPD-349](https://ripplelabs.atlassian.net/browse/RIPD-349)) -- Account for high-ASCII characters. ([RIPD-464](https://ripplelabs.atlassian.net/browse/RIPD-464)) - -**More information** - -For more information or assistance, the following resources will be of use: - -- [Ripple Developer Forums](https://ripple.com/forum/viewforum.php?f=2) -- [IRC](https://webchat.freenode.net/?channels=#ripple) - - ------------------------------------------------------------ - -## Version 0.26.2 - -rippled 0.26.2 has been released. The repository tag is *0.26.2* and can be found on GitHub at: - -Prior to building, please confirm you have the correct source tree with the **git log** command. The first log entry should be the change setting the version: - - commit b9454e0f0ca8dbc23844a0520d49394e10d445b1 - Author: Vinnie Falco - Date: Mon Aug 11 15:25:44 2014 -0400 - - Set version to 0.26.2 - -This release incorporates a small number of important bugfixes. Please refer to the [Git commit history](https://github.com/ripple/rippled/commits/0.26.2) for more detailed information. - -**Toolchain support** - -The minimum supported version of GCC used to compile rippled is v4.8. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Ubuntu_versions_older_than_13.10_:_Install_gcc_4.8) if you have not upgraded already. - -While it may be possible to compile rippled on (virtual) machines with 4GB of RAM, we recommend compiling on (virtual) machines with 8GB of RAM or more. If your build machine has more than one CPU (**\`grep '^processor' /proc/cpuinfo | wc -l\`**), you can use them to assist in the build process by compiling with the command **scons -j\[number of CPUs - 1\]**. - -The minimum supported version of Boost is v1.55.0. You **must** upgrade to this release or later to successfully compile this release of rippled. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Install_Boost) if you have not upgraded already. - -**New Features** - -- Freeze enforcement: activates on September 15, 2014 ([RIPD-399](https://ripplelabs.atlassian.net/browse/RIPD-399)) -- Add pubkey\_node and hostid to server stream messages ([RIPD-407](https://ripplelabs.atlassian.net/browse/RIPD-407)) - -**Bug Fixes** - -- Fix intermittent exception when closing HTTPS connections ([RIPD-475](https://ripplelabs.atlassian.net/browse/RIPD-475)) -- Correct Pathfinder::getPaths out to handle order books ([RIPD-427](https://ripplelabs.atlassian.net/browse/RIPD-427)) -- Detect inconsistency in PeerFinder self-connects ([RIPD-411](https://ripplelabs.atlassian.net/browse/RIPD-411)) - -**Experimental Features** - -- Add owner\_funds to client subscription data ([RIPD-377](https://ripplelabs.atlassian.net/browse/RIPD-377)) - -The offer funding status feature is “experimental” in this version. Developers are able to see the field, but it is subject to change in future releases. - -**More information** - -For more information or assistance, the following resources will be of use: - -- [Ripple Developer Forums](https://ripple.com/forum/viewforum.php?f=2) -- [IRC](https://webchat.freenode.net/?channels=#ripple) - - ------------------------------------------------------------ - -## Version 0.26.1 - -rippled v0.26.1 has been released. The repository tag is **0.26.1** and can be found on GitHub at: - -Prior to building, please confirm you have the correct source tree with the **git log** command. The first log entry should be the change setting the version: - - commit 9a0e806f78300374e20070e2573755fbafdbfd03 - Author: Vinnie Falco - Date: Mon Jul 28 11:27:31 2014 -0700 - - Set version to 0.26.1 - -This release incorporates a small number of important bugfixes. Please refer to the [Git commit history](https://github.com/ripple/rippled/commits/0.26.1) for more detailed information. - -**Toolchain support** - -The minimum supported version of GCC used to compile rippled is v4.8. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Ubuntu_versions_older_than_13.10_:_Install_gcc_4.8) if you have not upgraded already. - -While it may be possible to compile rippled on (virtual) machines with 4GB of RAM, we recommend compiling on (virtual) machines with 8GB of RAM or more. If your build machine has more than one CPU (**\`grep '^processor' /proc/cpuinfo | wc -l\`**), you can use them to assist in the build process by compiling with the command **scons -j\[number of CPUs - 1\]**. - -The minimum supported version of Boost is v1.55.0. You **must** upgrade to this release or later to successfully compile this release of rippled. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Install_Boost) if you have not upgraded already. - -**Bug Fixes** - -- Enabled asynchronous handling of HTTP-RPC interactions. This fixes client handlers using RPC that periodically return blank responses to requests. ([RIPD-390](https://ripplelabs.atlassian.net/browse/RIPD-390)) -- Fixed auth handling during OfferCreate. This fixes a regression of [RIPD-256](https://ripplelabs.atlassian.net/browse/RIPD-256). ([RIPD-414](https://ripplelabs.atlassian.net/browse/RIPD-414)) - -**More information** - -For more information or assistance, the following resources will be of use: - -- [Ripple Developer Forums](https://ripple.com/forum/viewforum.php?f=2) -- [IRC](https://webchat.freenode.net/?channels=#ripple) - - ------------------------------------------------------------ - -## Version 0.26.0 - -rippled v0.26.0 has been released. The repository tag is **0.26.0** and can be found on GitHub at: - -Prior to building, please confirm you have the correct source tree with the **git log** command. The first log entry should be the change setting the version: - - commit 9fa5e3987260e39dba322f218d39ac228a5b361b - Author: Vinnie Falco - Date: Tue Jul 22 09:59:45 2014 -0700 - - Set version to 0.26.0 - -This release incorporates a significant number of improvements and important bugfixes. Please refer to the [Git commit history](https://github.com/ripple/rippled/commits/develop) for more detailed information. - -**Toolchain support** - -The minimum supported version of GCC used to compile rippled is v4.8. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Ubuntu_versions_older_than_13.10_:_Install_gcc_4.8) if you have not upgraded already. - -While it may be possible to compile rippled on (virtual) machines with 4GB of RAM, we recommend compiling on (virtual) machines with 8GB of RAM or more. If your build machine has more than one CPU (**\`grep '^processor' /proc/cpuinfo | wc -l\`**), you can use them to assist in the build process by compiling with the command **scons -j\[number of CPUs - 1\]**. - -The minimum supported version of Boost is v1.55.0. You **must** upgrade to this release or later to successfully compile this release of rippled. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Install_Boost) if you have not upgraded already. - -**Improvements** - -- Updated integration tests. -- Updated tests for account freeze functionality. -- Implement setting the no-freeze flag on Ripple accounts ([RIPD-394](https://ripplelabs.atlassian.net/browse/RIPD-394)). -- Improve transaction fee and execution logic ([RIPD-323](https://ripplelabs.atlassian.net/browse/RIPD-323)). -- Implemented finding of 'sabfd' paths ([RIPD-335](https://ripplelabs.atlassian.net/browse/RIPD-335)). -- Imposed a local limit on paths lengths ([RIPD-350](https://ripplelabs.atlassian.net/browse/RIPD-350)). -- Documented [ledger entries](https://github.com/ripple/rippled/blob/develop/src/ripple/module/app/ledger/README.md) ([RIPD-361](https://ripplelabs.atlassian.net/browse/RIPD-361)). -- Documented [SHAMap](https://github.com/ripple/rippled/blob/develop/src/ripple/module/app/shamap/README.md). - -**Bug Fixes** - -- Fixed the limit parameter on book\_offers ([RIPD-295](https://ripplelabs.atlassian.net/browse/RIPD-295)). -- Removed SHAMapNodeID from SHAMapTreeNode to fix "right data, wrong ID" bug in the tree node cache ([RIPD-347](https://ripplelabs.atlassian.net/browse/RIPD-347)). -- Eliminated spurious SHAMap::getFetchPack failure ([RIPD-379](https://ripplelabs.atlassian.net/browse/RIPD-379)). -- Disabled SSLv2. -- Implemented rate-limiting of SSL client renegotiation to mitigate [SCIR DoS vulnerability](https://www.thc.org/thc-ssl-dos/) ([RIPD-360](https://ripplelabs.atlassian.net/browse/RIPD-360)). -- Display unprintable or malformatted currency codes as hex digits. -- Fix static initializers in RippleSSLContext ([RIPD-375](https://ripplelabs.atlassian.net/browse/RIPD-375)). - -**More information** - -For more information or assistance, the following resources will be of use: - -- [Ripple Developer Forums](https://ripple.com/forum/viewforum.php?f=2) -- [IRC](https://webchat.freenode.net/?channels=#ripple) - - ------------------------------------------------------------ - -## Version 0.25.2 - -rippled v0.25.2 has been released. The repository tag is **0.25.2** and can be found on GitHub at: - -Prior to building, please confirm you have the correct source tree with the **git log** command. The first log entry should be the change setting the version: - - commit ddf68d464d74e1c76a0cfd100a08bc8e65b91fec - Author: Mark Travis - Date: Mon Jul 7 11:46:15 2014 -0700 - - Set version to 0.25.2 - -This release incorporates significant improvements which may not warrant separate entries but are incorporated into the feature changes as summary lines. Please refer to the [Git commit history](https://github.com/ripple/rippled/commits/develop) for more information. - -**Toolchain support** - -The minimum supported version of GCC used to compile rippled is v4.8. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Ubuntu_versions_older_than_13.10_:_Install_gcc_4.8) if you have not upgraded already. - -While it may be possible to compile rippled on (virtual) machines with 4GB of RAM, we recommend build machines with 8GB of RAM. - -The minimum supported version of Boost is v1.55. You **must** upgrade to this release or later to successfully compile this release. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Install_Boost) if you have not upgraded already. - -**Improvements** - -- CPU utilization for certain operations has been optimized. -- Improve serialization of public ledger blocks. -- rippled now takes much less time to compile. -- Additional pathfinding heuristic: increases liquidity in some cases. - -**Bug Fixes** - -- Unprintable currency codes will be printed as hex digits. -- Transactions with unreasonably long path lengths are rejected. The maximum is now eight (8) hops. - - ------------------------------------------------------------ - -## Version 0.25.1 - -`rippled` v0.25.1 has been released. The repository tag is `0.25.1` and can be found on GitHub at: https://github.com/ripple/rippled/tree/0.25.1 - -Prior to building, please confirm you have the correct source tree with the `git log` command. The first log entry should be the change setting the version: - - commit b677cacb8ce0d4ef21f8c60112af1db51dce5bb4 - Author: Vinnie Falco - Date: Thu May 15 08:27:20 2014 -0700 - - Set version to 0.25.1 - -This release incorporates significant improvements which may not warrant separate entries but are incorporated into the feature changes as summary lines. Please refer to the [Git commit history](https://github.com/ripple/rippled/commits/develop) for more information. - -**Toolchain support** - -The minimum supported version of GCC used to compile rippled is v4.8. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Ubuntu_versions_older_than_13.10_:_Install_gcc_4.8) if you have not upgraded already. - -A minimum of 4GB of RAM are required to successfully compile this release. - -The minimum supported version of Boost is v1.55. You **must** upgrade to this release or later to successfully compile this release. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Install_Boost) if you have not upgraded already. - -**Major Features** - -* Option to compress the NodeStore db. More speed, less space. See [`rippled-example.cfg`](https://github.com/ripple/rippled/blob/0.25.1/doc/rippled-example.cfg#L691) - -**Improvements** - -* Remove redundant checkAccept call -* Added I/O latency to output of ''server_info''. -* Better performance handling of Fetch Packs. -* Improved handling of modified ledger nodes. -* Improved performance of JSON document generator. -* Made strConcat operate in O(n) time for greater efficiency. -* Added some new configuration options to doc/rippled-example.cfg - -**Bug Fixes** - -* Fixed a bug in Unicode parsing of transactions. -* Fix a blocker with tfRequireAuth -* Merkle tree nodes that are retrieved as a result of client requests are cached locally. -* Use the last ledger node closed for finding old paths through the network. -* Reduced number of asynchronous fetches. - - ------------------------------------------------------------ - -## Version 0.25.0 - -rippled version 0.25.0 has been released. The repository tag is **0.25.0** and can be found on GitHub at: - -Prior to building, please confirm you have the correct source tree with the **git log** command. The first log entry should be the change setting the version: - - commit 29d1d5f06261a93c5e94b4011c7675ff42443b7f - Author: Vinnie Falco - Date: Wed May 14 09:01:44 2014 -0700 - - Set version to 0.25.0 - -This release incorporates significant improvements which may not warrant separate entries but are incorporated into the feature changes as summary lines. Please refer to the [Git commit history](https://github.com/ripple/rippled/commits/develop) for more information. - -**Toolchain support** - -The minimum supported version of GCC used to compile rippled is v4.8. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Ubuntu_versions_older_than_13.10_:_Install_gcc_4.8) if you have not upgraded already. - -A minimum of 4GB of RAM are required to successfully compile this release. - -The minimum supported version of Boost is v1.55. You **must** upgrade to this release or later to successfully compile this release. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Install_Boost) if you have not upgraded already. - -**Major Features** - -- Option to compress the NodeStore db. More speed, less space. See [`rippled-example.cfg`](https://github.com/ripple/rippled/blob/0.25.0/doc/rippled-example.cfg#L691) - -**Improvements** - -- Remove redundant checkAccept call -- Added I/O latency to output of *server\_info*. -- Better performance handling of Fetch Packs. -- Improved handling of modified ledger nodes. -- Improved performance of JSON document generator. -- Made strConcat operate in O(n) time for greater efficiency. - -**Bug Fixes** - -- Fix a blocker with tfRequireAuth -- Merkle tree nodes that are retrieved as a result of client requests are cached locally. -- Use the last ledger node closed for finding old paths through the network. -- Reduced number of asynchronous fetches. - - ------------------------------------------------------------ - -## Version 0.24.0 - -rippled version 0.24.0 has been released. The repository tag is **0.24.0** and can be found on GitHub at: - -Prior to building, please confirm you have the correct source tree with the **git log** command. The first log entry should be the change setting the version: - - commit 3eb1c7bd6f93e5d874192197f76571184338f702 - Author: Vinnie Falco - Date: Mon May 5 10:20:46 2014 -0700 - - Set version to 0.24.0 - -This release incorporates significant improvements which may not warrant separate entries but are incorporated into the feature changes as summary lines. Please refer to the [Git commit history](https://github.com/ripple/rippled/commits/develop) for more information. - -**Toolchain support** - -The minimum supported version of GCC used to compile rippled is v4.8. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Ubuntu_versions_older_than_13.10_:_Install_gcc_4.8) if you have not upgraded already. - -A minimum of 4GB of RAM are required to successfully compile this release. - -The minimum supported version of Boost is v1.55. You **must** upgrade to this release or later to successfully compile this release. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Install_Boost) if you have not upgraded already. - -**Improvements** - -- Implemented logic for ledger processes and features. -- Use "high threads" for background RocksDB database writes. -- Separately track locally-issued transactions to ensure they always appear in the open ledger. - -**Bug Fixes** - -- Fix AccountSet for canonical transactions. -- The RPC [sign](https://ripple.com/build/rippled-apis/#sign) command will now sign with either an account's master or regular secret key. -- Fixed out-of-order network initialization. -- Improved efficiency of pathfinding for transactions. -- Reworked timing of ledger validation and related operations to fix race condition against the network. -- Build process enforces minimum versions of OpenSSL and BOOST for operation. - - ------------------------------------------------------------ - -## Version 0.23.0 - -rippled version 0.23.0 has been released. The repository tag is **0.23.0** and can be found on GitHub at: - -Prior to building, please confirm you have the correct source tree with the **git log** command. The first log entry should be the change setting the version: - - commit 29a4f61551236f70865d46d6653da2e62de1c701 - Author: Vinnie Falco - Date: Fri Mar 14 13:01:23 2014 -0700 - - Set version to 0.23.0 - -This release incorporates significant improvements which may not warrant separate entries but are incorporated into the feature changes as summary lines. Please refer to the [Git commit history](https://github.com/ripple/rippled/commits/develop) for more information. - -**Toolchain support** - -The minimum supported version of GCC used to compile rippled is v4.8. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Ubuntu_versions_older_than_13.10_:_Install_gcc_4.8) if you have not upgraded already. - -A minimum of 4GB of RAM are required to successfully compile this release. - -The minimum supported version of Boost is v1.55. You **must** upgrade to this release or later to successfully compile this release. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Install_Boost) if you have not upgraded already. - -**Improvements** - -- Allow the word 'none' in the *.cfg* file to disable storing historical ledgers. -- Clarify the initialization of hash prefixes used in the *RadMap*. -- Better validation of RPC-JSON from all sources -- Reduce spurious log output from Peers -- Eliminated some I/O for certain operations in the *RadMap*. -- Client requests for full state trees now require administrative privileges. -- Added "MemoData" field for transaction memos. -- Prevent the node cache from overflowing needlessly in certain cases -- Add "ledger\_data" command for retrieving entire ledgers in chunks. -- Reduce the quantity of forwarded transactions and proposals in some cases -- Improved diagnostics when errors occur loading SSL certificates - -**Bug Fixes** - -- Fix rare crash when a race condition involving disconnecting sockets occurs -- Fix a corner case with hex conversion of strings with odd character lengths -- Fix an exception in a corner case when erroneous transactions were being logged -- Fix the treatment of expired offers when cleaning up offers -- Prevent a needless transactor from being created if the tx ID is not valid -- Fix the peer action transition from "syncing" to "full" -- Fix error reporting for unknown inner JSON fields -- Fix source file path displayed when an assertion failure is reported -- Fix typos in transaction engine error code identifiers - - ------------------------------------------------------------ - -## Version 0.22.0 - -rippled version 0.22.0 has been released. This release is currently the tip of the **develop/** branch and can be found on GitHub at: The tag is **0.22.0** and can be found on GitHub at: - -**This is a critical release affecting transaction processing. All partners should update immediately.** - -Prior to building, please confirm you have the correct source tree with the **git log** command. The first log entry should be the change setting the version: - -This release incorporates significant improvements which may not warrant separate entries but are incorporated into the feature changes as summary lines. Please refer to the [Git commit history](https://github.com/ripple/rippled/commits/develop) for more information. - -**Toolchain support** - -The minimum supported version of GCC used to compile rippled is v4.8. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Ubuntu_versions_older_than_13.10_:_Install_gcc_4.8) if you have not upgraded already. - -A minimum of 4GB of RAM are required to successfully compile this release. - -The minimum supported version of libBOOST is v1.55. You **must** upgrade to this release or later to successfully compile this release. Please follow [these instructions](https://wiki.ripple.com/Ubuntu_build_instructions#Install_Boost) if you have not upgraded already. - -**Key release features** - -- **PeerFinder** - - - Actively guides network topology. - - Scrubs listening advertisements based on connectivity checks. - - Redirection for new nodes when existing nodes are full. - -- **Memos** - - - Transactions can optionally include a short text message, which optionally can be encrypted. - -- **Database** - - - Improved management of I/O resources. - - Better performance accessing historical data. - -- **PathFinding** - - - More efficient search algorithm when computing paths - -**Major Partner Issues Fixed** - -- **Transactions** - - - Malleability: Ability to ensure that signatures are fully canonical. - -- **PathFinding** - - - Less time needed to get the first path result! - -- **Database** - - - Eliminated "meltdowns" caused when fetching historical ledger data. - -**Significant Changes** - -- Cleaned up logic which controls when ledgers are fetched and under what conditions. -- Cleaned up file path calculation for database files. -- Changed dispatcher for WebSocket requests. -- Cleaned up multithreading mechanisms. -- Fixed custom currency code parsing. -- Optimized transaction node lookup circumstances in the node store. - - ------------------------------------------------------------ - -## Version 0.21.0 - -rippled version 0.21.0 has been released. This release is currently the tip of the **develop/** branch and can be found on GitHub at [1](https://github.com/ripple/rippled/tree/develop). The tag is **0.21.0-rc2** and can be found on GitHub at [2](https://github.com/ripple/rippled/tree/0.21.0-rc2). - -**This is a critical release. All partners should update immediately.** - -Prior to building, please confirm you have the correct source tree with the **git log** command. The first log entry should be the change setting the version: - - commit f295bb20a16d1d2999f606c1297c8930d8e33c40 - Author: JoelKatz - Date: Fri Jan 24 11:17:16 2014 -0800 - - Set version to 0.21.0.rc2 - -**Major Partner Issues Fixed** - -- Order book issues - - Ensure all crossing offers are taken - - Ensure order book is not left crossed -- Added **DeliveredAmount** field to transaction metadata - - Reports amount delivered in partial payments - -**Toolchain support** - -As with the previous release, the minimum supported version of GCC used to compile rippled is v4.8. - -**Significant Changes** - -- Pairwise no-ripple - - Permits trust lines to be protected from rippling - - Operates on protected pairs -- Performance improvements - - Improve I/O latency - - Improve fetching ledgers - - Improve pathfinding -- Features for robust transaction submission - - LastLedgerSeq for transaction expiration - - AccountTxnID for transaction chaining -- Fix some cases where an invalid transaction would stay in limbo -- Code cleanups -- Better reporting of invalid parameters - -**Release Candidates** - -RC1 fixed performance problems with order book retrieval. - -RC2 fixed a bug that caused crashes in order processing and a bug in parsing order book requests. - -**Notice** - -If you are upgrading from version 0.12 or earlier of rippled, these next sections apply to you because the format of the *rippled.cfg* file changed around that time. If you have upgraded since that time and you have applied the configuration file fixes, you can safely ignore them. - -**Validators** - -Ripple Labs is now running five validators. You can use this template for your *validators.txt* file (or place this in your config file): - - [validators] - n9KPnVLn7ewVzHvn218DcEYsnWLzKerTDwhpofhk4Ym1RUq4TeGw RIP1 - n9LFzWuhKNvXStHAuemfRKFVECLApowncMAM5chSCL9R5ECHGN4V RIP2 - n94rSdgTyBNGvYg8pZXGuNt59Y5bGAZGxbxyvjDaqD9ceRAgD85P RIP3 - n9LeQeDcLDMZKjx1TZtrXoLBLo5q1bR1sUQrWG7tEADFU6R27UBp RIP4 - n9KF6RpvktjNs2MDBkmxpJbup4BKrKeMKDXPhaXkq7cKTwLmWkFr RIP5 - -You should also raise your quorum to at least three by putting the following in your *rippled.cfg* file: - - [validation_quorum] - 3 - -If you are a validator, you should set your quorum to at least four. - -**IPs** - -A list of Ripple Labs server IP addresses can be found by resolving **r.ripple.com**. You can also add this to your *rippled.cfg* file to ensure you always have several peer connections to Ripple Labs servers: - - [ips] - 184.73.226.101 51235 - 23.23.201.55 51235 - 54.200.43.173 51235 - 184.73.57.84 51235 - 54.234.249.55 51235 - 54.200.86.110 51235 - -**RocksDB back end** - -RocksDB is based on LevelDB with improvements from Facebook and the community. Preliminary tests show that it stalls less often than HyperLevelDB for our use cases. - -If you are switching over from an existing back end, you have two options. You can remove your old database and let rippled recreate it as it re-syncs, or you can import your old database into the new one. - -To remove your old database, make sure the server is shut down (\`rippled stop\`). Remove the *db/ledger.db* and *db/transaction.db* files. Remove all the files in your back end store directory (*db/hashnode* by default). Then change your configuration file to use the RocksDB back end and restart. - -To import your old database, start by shutting the server down. Then modify the configuration file by renaming your *\[node\_db\]* stanza to *\[import\_db\]*. Create a new *\[node\_db\]* stanza and specify a RocksDB back end with a different directory. Start the server with the command **rippled --import**. When the import finishes gracefully stop the server (\`rippled stop\`). Please wait for rippled to stop on its own because it can take several minutes for it to shut down after an import. Remove the old database, put the new database into place, remove the *\[import\_db\]* section, change the *\[node\_db\]* section to refer to the final location, and restart the server. - -The recommended RocksDB configuration is: - - [node_db] - type=RocksDB - path=db/hashnode - open_files=1200 - filter_bits=12 - cache_mb=128 - file_size_mb=8 - file_size_mult=2 - -**Configuring your Node DB** - -You need to configure the [NodeBackEnd](https://wiki.ripple.com/NodeBackEnd) that you want the server to use. See above for an example RocksDB configuration. - -- **Note**: HyperLevelDB and RocksDB are not available on Windows platform. - - ------------------------------------------------------------ - -## Version 0.20.1 - -rippled version 0.20.1 has been released. This release is currently the tip of the [develop](https://github.com/ripple/rippled/tree/develop) branch and the tag is [0.20.1](https://github.com/ripple/rippled/tree/0.20.1). - -**This is a critical release. All partners should update immediately.** - -Prior to building, please confirm you have the correct source tree with the **git log** command. The first log entry should be the change setting the version: - - commit 95a573b755219d7e1e078d53b8e11a8f0d7cade1 - Author: Vinnie Falco - Date: Wed Jan 8 17:08:27 2014 -0800 - - Set version to 0.20.1 - -**Major Partner Issues Fixed** - -- rippled will crash randomly. - - Entries in the three parts of the order book are missing or do not match. In such a case, rippled will crash. -- Server loses sync randomly. - - This is due to rippled restarting after it crashes. That the server restarted is not obvious and appears to be something else. -- Server goes 'offline' randomly. - - This is due to rippled restarting after it crashes. That the server restarted is not obvious and appears to be something else. -- **complete\_ledgers** part of **server\_info** output says "None". - - This is due to rippled restarting and reconstructing the ledger after it crashes. - - If the node back end is corrupted or has been moved without being renamed in rippled.cfg, this can cause rippled to crash and restart. - -**Toolchain support** - -Starting with this release, the minimum supported version of GCC used to compile rippled is v4.8. - -**Significant Changes** - -- Don't log StatsD messages to the console by default. -- Fixed missing jtACCEPT job limit. -- Removed dead code to clean up the codebase. -- Reset liquidity before retrying rippleCalc. -- Made improvements becuase items in SHAMaps are immutable. -- Multiple pathfinding bugfixes: - - Make each path request track whether it needs updating. - - Improve new request handling, reverse order for processing requests. - - Break to handle new requests immediately. - - Make mPathFindThread an integer rather than a bool. Allow two threads. - - Suspend processing requests if server is backed up. - - Multiple performance improvements and enhancements. - - Fixed locking. -- Refactored codebase to make it C++11 compliant. -- Multiple fixes to ledger acquisition, cleanup, and logging. -- Made multiple improvements to WebSockets server. -- Added Debian-style initscript (doc/rippled.init). -- Updated default config file (doc/rippled-example.cfg) to reflect best practices. -- Made changes to SHAMapTreeNode and visitLeavesInternal to conserve memory. -- Implemented new fee schedule: - - Transaction fee: 10 drops - - Base reserve: 20 XRP - - Incremental reserve: 5 XRP -- Fixed bug \#211 (getTxsAccountB in NetworkOPs). -- Fixed a store/fetch race condition in ther node back end. -- Fixed multiple comparison operations. -- Removed Sophia and Lightning databases. - -**Notice** - -If you are upgrading from version 0.12 or earlier of rippled, these next sections apply to you because the format of the *rippled.cfg* file changed around that time. If you have upgraded since that time and you have applied the configuration file fixes, you can safely ignore them. - -**Validators** - -Ripple Labs is now running five validators. You can use this template for your *validators.txt* file (or place this in your config file): - - [validators] - n9KPnVLn7ewVzHvn218DcEYsnWLzKerTDwhpofhk4Ym1RUq4TeGw RIP1 - n9LFzWuhKNvXStHAuemfRKFVECLApowncMAM5chSCL9R5ECHGN4V RIP2 - n94rSdgTyBNGvYg8pZXGuNt59Y5bGAZGxbxyvjDaqD9ceRAgD85P RIP3 - n9LeQeDcLDMZKjx1TZtrXoLBLo5q1bR1sUQrWG7tEADFU6R27UBp RIP4 - n9KF6RpvktjNs2MDBkmxpJbup4BKrKeMKDXPhaXkq7cKTwLmWkFr RIP5 - -You should also raise your quorum to at least three by putting the following in your *rippled.cfg* file: - - [validation_quorum] - 3 - -If you are a validator, you should set your quorum to at least four. - -**IPs** - -A list of Ripple Labs server IP addresses can be found by resolving **r.ripple.com**. You can also add this to your *rippled.cfg* file to ensure you always have several peer connections to Ripple Labs servers: - - [ips] - 54.225.112.220 51235 - 54.225.123.13 51235 - 54.227.239.106 51235 - 107.21.251.218 51235 - 184.73.226.101 51235 - 23.23.201.55 51235 - -**New RocksDB back end** - -RocksDB is based on LevelDB with improvements from Facebook and the community. Preliminary tests show that it stalls less often than HyperLevelDB for our use cases. - -If you are switching over from an existing back end, you have two options. You can remove your old database and let rippled recreate it as it re-syncs, or you can import your old database into the new one. - -To remove your old database, make sure the server is shut down (`rippled stop`). Remove the *db/ledger.db* and *db/transaction.db* files. Remove all the files in your back end store directory (*db/hashnode* by default). Then change your configuration file to use the RocksDB back end and restart. - -To import your old database, start by shutting the server down. Then modify the configuration file by renaming your *\[node\_db\]* stanza to *\[import\_db\]*. Create a new *\[node\_db\]* stanza and specify a RocksDB back end with a different directory. Start the server with the command **rippled --import**. When the import finishes gracefully stop the server (`rippled stop`). Please wait for rippled to stop on its own because it can take several minutes for it to shut down after an import. Remove the old database, put the new database into place, remove the *\[import\_db\]* section, change the *\[node\_db\]* section to refer to the final location, and restart the server. - -The recommended RocksDB configuration is: - - [node_db] - type=RocksDB - path=db/hashnode - open_files=1200 - filter_bits=12 - cache_mb=256 - file_size_mb=8 - file_size_mult=2 - -**Configuring your Node DB** - -You need to configure the [NodeBackEnd](https://wiki.ripple.com/NodeBackEnd) that you want the server to use. See above for an example RocksDB configuration. - -- **Note**: HyperLevelDB and RocksDB are not available on Windows platform. - - ------------------------------------------------------------ - -## Version 0.19 - -rippled version 0.19 has now been released. This release is currently the tip of the [release](https://github.com/ripple/rippled/tree/release) branch and the tag is [0.19.0](https://github.com/ripple/rippled/tree/0.19.0). - -Prior to building, please confirm you have the correct source tree with the `git log` command. The first log entry should be the change setting the version: - - commit 26783607157a8b96e6e754f71565f4eb0134efc1 - Author: Vinnie Falco - Date: Fri Nov 22 23:36:50 2013 -0800 - - Set version to 0.19.0 - -**Significant Changes** - -- Bugfixes and improvements in path finding, path filtering, and payment execution. -- Updates to HyperLevelDB and LevelDB node storage back ends. -- Addition of RocksDB node storage back end. -- New resource manager for tracking server load. -- Fixes for a few bugs that can crashes or inability to serve client requests. - -**Validators** - -Ripple Labs is now running five validators. You can use this template for your `validators.txt` file (or place this in your config file): - - [validators] - n9KPnVLn7ewVzHvn218DcEYsnWLzKerTDwhpofhk4Ym1RUq4TeGw RIP1 - n9LFzWuhKNvXStHAuemfRKFVECLApowncMAM5chSCL9R5ECHGN4V RIP2 - n94rSdgTyBNGvYg8pZXGuNt59Y5bGAZGxbxyvjDaqD9ceRAgD85P RIP3 - n9LeQeDcLDMZKjx1TZtrXoLBLo5q1bR1sUQrWG7tEADFU6R27UBp RIP4 - n9KF6RpvktjNs2MDBkmxpJbup4BKrKeMKDXPhaXkq7cKTwLmWkFr RIP5 - -You should also raise your quorum to at least three by putting the following in your `rippled.cfg` file: - - [validation_quorum] - 3 - -If you are a validator, you should set your quorum to at least four. - -**IPs** - -A list of Ripple Labs server IP addresses can be found by resolving `r.ripple.com`. You can also add this to your `rippled.cfg` file to ensure you always have several peer connections to Ripple Labs servers: - - [ips] - 54.225.112.220 51235 - 54.225.123.13 51235 - 54.227.239.106 51235 - 107.21.251.218 51235 - 184.73.226.101 51235 - 23.23.201.55 51235 - -**New RocksDB back end** - -RocksDB is based on LevelDB with improvements from Facebook and the community. Preliminary tests show that it stall less often than HyperLevelDB. - -If you are switching over from an existing back end, you have two choices. You can remove your old database or you can import it. - -To remove your old database, make sure the server is shutdown. Remove the `db/ledger.db` and `db/transaction.db` files. Remove all the files in your back end store directory, `db/hashnode` by default. Then you can change your configuration file to use the RocksDB back end and restart. - -To import your old database, start by shutting the server down. Then modify the configuration file by renaming your `[node_db]` portion to `[import_db]`. Create a new `[node_db]` section specify a RocksDB back end and a different directory. Start the server with `rippled --import`. When the import finishes, stop the server (it can take several minutes to shut down after an import), remove the old database, put the new database into place, remove the `[import_db]` section, change the `[node_db]` section to refer to the final location, and restart the server. - -The recommended RocksDB configuration is: - - [node_db] - type=RocksDB - path=db/hashnode - open_files=1200 - filter_bits=12 - cache_mb=256 - file_size_mb=8 - file_size_mult=2 - -**Configuring your Node DB** - -You need to configure the [NodeBackEnd](https://wiki.ripple.com/NodeBackEnd) that you want the server to use. See above for an example RocksDB configuration. - -- **Note:** HyperLevelDB and RocksDB are not available on Windows platform. - - ------------------------------------------------------------ - -## Version 0.16 - -rippled version 0.16 has now been released. This release is currently the tip of the [master](https://github.com/ripple/rippled/tree/master) branch and the tag is [v0.16.0](https://github.com/ripple/rippled/tree/v0.16.0). - -Prior to building, please confirm you have the correct source tree with the `git log` command. The first log entry should be the change setting the version: - - commit 15ef43505473225af21bb7b575fb0b628d5e7f73 - Author: vinniefalco - Date: Wed Oct 2 2013 - - Set version to 0.16.0 - -**Significant Changes** - -- Improved peer discovery -- Improved pathfinding -- Ledger speed improvements -- Reduced memory consumption -- Improved server stability -- rippled no longer throws and exception on exiting -- Better error reporting -- Ripple-lib tests have been ported to use the Mocha testing framework - -**Validators** - -Ripple Labs is now running five validators. You can use this template for your `validators.txt` file: - - [validators] - n9KPnVLn7ewVzHvn218DcEYsnWLzKerTDwhpofhk4Ym1RUq4TeGw RIP1 - n9LFzWuhKNvXStHAuemfRKFVECLApowncMAM5chSCL9R5ECHGN4V RIP2 - n94rSdgTyBNGvYg8pZXGuNt59Y5bGAZGxbxyvjDaqD9ceRAgD85P RIP3 - n9LeQeDcLDMZKjx1TZtrXoLBLo5q1bR1sUQrWG7tEADFU6R27UBp RIP4 - n9KF6RpvktjNs2MDBkmxpJbup4BKrKeMKDXPhaXkq7cKTwLmWkFr RIP5 - -You should also raise your quorum to at least three by putting the following in your `rippled.cfg` file: - - [validation_quorum] - 3 - -If you are a validator, you should set your quorum to at least four. - -**IPs** - -A list of Ripple Labs server IP addresses can be found by resolving `r.ripple.com`. You can also add this to your `rippled.cfg` file to ensure you always have several peer connections to Ripple Labs servers: - - [ips] - 54.225.112.220 51235 - 54.225.123.13 51235 - 54.227.239.106 51235 - 107.21.251.218 51235 - 184.73.226.101 51235 - 23.23.201.55 51235 - -**Node DB** - -You need to configure the [NodeBackEnd](https://wiki.ripple.com/NodeBackEnd) that you want the server to use. In most cases, that will mean adding this to your configuration file: - - [node_db] - type=HyperLevelDB - path=db/hashnode - -- NOTE HyperLevelDB is not available on Windows platforms. - -**Release Candidates** - -**Issues** - -None known - - ------------------------------------------------------------ - -## Version 0.14 - -rippled version 0.14 has now been released. This release is currently the tip of the [master](https://github.com/ripple/rippled/tree/master) branch and the tag is [v0.12.0](https://github.com/ripple/rippled/tree/v0.14.0). - -Prior to building, please confirm you have the correct source tree with the `git log` command. The first log entry should be the change setting the version: - - commit b6d11c08d0245ee9bafbb97143f5d685dd2979fc - Author: vinniefalco - Date: Wed Oct 2 2013 - - Set version to 0.14.0 - -**Significant Changes** - -- Improved peer discovery -- Improved pathfinding -- Ledger speed improvements -- Reduced memory consumption -- Improved server stability -- rippled no longer throws and exception on exiting -- Better error reporting -- Ripple-lib tests have been ported to use the Mocha testing framework - -**Validators** - -Ripple Labs is now running five validators. You can use this template for your `validators.txt` file: - - [validators] - n9KPnVLn7ewVzHvn218DcEYsnWLzKerTDwhpofhk4Ym1RUq4TeGw RIP1 - n9LFzWuhKNvXStHAuemfRKFVECLApowncMAM5chSCL9R5ECHGN4V RIP2 - n94rSdgTyBNGvYg8pZXGuNt59Y5bGAZGxbxyvjDaqD9ceRAgD85P RIP3 - n9LeQeDcLDMZKjx1TZtrXoLBLo5q1bR1sUQrWG7tEADFU6R27UBp RIP4 - n9KF6RpvktjNs2MDBkmxpJbup4BKrKeMKDXPhaXkq7cKTwLmWkFr RIP5 - -You should also raise your quorum to at least three by putting the following in your `rippled.cfg` file: - - [validation_quorum] - 3 - -If you are a validator, you should set your quorum to at least four. - -**IPs** - -A list of Ripple Labs server IP addresses can be found by resolving `r.ripple.com`. You can also add this to your `rippled.cfg` file to ensure you always have several peer connections to Ripple Labs servers: - - [ips] - 54.225.112.220 51235 - 54.225.123.13 51235 - 54.227.239.106 51235 - 107.21.251.218 51235 - 184.73.226.101 51235 - 23.23.201.55 51235 - -**Node DB** - -You need to configure the [NodeBackEnd](https://wiki.ripple.com/NodeBackEnd) that you want the server to use. In most cases, that will mean adding this to your configuration file: - - [node_db] - type=HyperLevelDB - path=db/hashnode - -- NOTE HyperLevelDB is not available on Windows platforms. - -**Release Candidates** - -**Issues** - -None known - - ------------------------------------------------------------ - -## Version 0.12 - -rippled version 0.12 has now been released. This release is currently the tip of the [master branch](https://github.com/ripple/rippled/tree/master) and can be found on GitHub. The tag is [v0.12.0](https://github.com/ripple/rippled/tree/v0.12.0). - -Prior to building, please confirm you have the correct source tree with the `git log` command. The first log entry should be the change setting the version: - - commit d0a9da6f16f4083993e4b6c5728777ffebf80f3a - Author: JoelKatz - Date: Mon Aug 26 12:08:05 2013 -0700 - - Set version to v0.12.0 - -**Major Partner Issues Fixed** - -- Server Showing "Offline" - -This issue was caused by LevelDB periodically compacting its internal data structure. While compacting, rippled's processing would stall causing the node to lose sync with the rest of the network. This issue was solved by switching from LevelDB to HyperLevelDB. rippled operators will need to change their ripple.cfg file. See below for configuration details. - -- Premature Validation of Transactions - -On rare occasions, a transaction would show as locally validated before the full network consensus was confirmed. This issue was resolved by changing the way transactions are saved. - -- Missing Ledgers - -Occasionally, some rippled servers would fail to fetch all ledgers. This left gaps in the local history and caused some API calls to report incomplete results. The ledger fetch code was rewritten to both prevent this and to repair any existing gaps. - -**Significant Changes** - -- The way transactions are saved has been changed. This fixes a number of ways transactions can incorrectly be reported as fully-validated. -- `doTransactionEntry` now works against open ledgers. -- `doLedgerEntry` now supports a binary option. -- A bug in `getBookPage` that caused it to skip offers is fixed. -- `getNodeFat` now returns deeper chains, reducing ledger acquire latency. -- Catching up if the (published ledger stream falls behind the network) is now more aggressive. -- I/O stalls are drastically reduced by using the HyperLevelDB node back end. -- Persistent ledger gaps should no longer occur. -- Clusters now exchange load information. - -**Validators** - -Ripple Labs is now running five validators. You can use this template for your `validators.txt` file: - - - - [validators] - n9KPnVLn7ewVzHvn218DcEYsnWLzKerTDwhpofhk4Ym1RUq4TeGw RIP1 - n9LFzWuhKNvXStHAuemfRKFVECLApowncMAM5chSCL9R5ECHGN4V RIP2 - n94rSdgTyBNGvYg8pZXGuNt59Y5bGAZGxbxyvjDaqD9ceRAgD85P RIP3 - n9LeQeDcLDMZKjx1TZtrXoLBLo5q1bR1sUQrWG7tEADFU6R27UBp RIP4 - n9KF6RpvktjNs2MDBkmxpJbup4BKrKeMKDXPhaXkq7cKTwLmWkFr RIP5 - - - -**Update April 2014** - Due to a vulnerability in OpenSSL the validator keys above have been cycled out, the five validators by RippleLabs use the following keys now: - - [validators] - n949f75evCHwgyP4fPVgaHqNHxUVN15PsJEZ3B3HnXPcPjcZAoy7 RL1 - n9MD5h24qrQqiyBC8aeqqCWvpiBiYQ3jxSr91uiDvmrkyHRdYLUj RL2 - n9L81uNCaPgtUJfaHh89gmdvXKAmSt5Gdsw2g1iPWaPkAHW5Nm4C RL3 - n9KiYM9CgngLvtRCQHZwgC2gjpdaZcCcbt3VboxiNFcKuwFVujzS RL4 - n9LdgEtkmGB9E2h3K4Vp7iGUaKuq23Zr32ehxiU8FWY7xoxbWTSA RL5 - -You should also raise your quorum to at least three by putting the following in your `rippled.cfg` file: - - [validation_quorum] - 3 - -If you are a validator, you should set your quorum to at least four. - -**IPs** - -A list of Ripple Labs server IP addresses can be found by resolving `r.ripple.com`. You can also add this to your `rippled.cfg` file to ensure you always have several peer connections to Ripple Labs servers: - - [ips] - 54.225.112.220 51235 - 54.225.123.13 51235 - 54.227.239.106 51235 - 107.21.251.218 51235 - 184.73.226.101 51235 - 23.23.201.55 51235 - -**Node DB** - -You need to configure the [NodeBackEnd](https://wiki.ripple.com/NodeBackEnd) that you want the server to use. In most cases, that will mean adding this to your configuration file: - - [node_db] - type=HyperLevelDB - path=db/hashnode - -- NOTE HyperLevelDB is not available on Windows platforms. - -**Release Candidates** - -RC1 was the first release candidate. - -RC2 fixed a bug that could cause ledger acquires to stall. - -RC3 fixed compilation under OSX. - -RC4 includes performance improvements in countAccountTx and numerous small fixes to ledger acquisition. - -RC5 changed the peer low water mark from 4 to 10 to acquire more server connections. - -RC6 fixed some possible load issues with the network state timer and cluster reporting timers. - -**Issues** - -Fetching of historical ledgers is slower in this build than in previous builds. This is being investigated. diff --git a/SECURITY.md b/SECURITY.md index 4e845735d4..eb7437d2f9 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -83,7 +83,7 @@ To report a qualifying bug, please send a detailed report to: |Long Key ID | `0xCD49A0AFC57929BE` | |Fingerprint | `24E6 3B02 37E0 FA9C 5E96 8974 CD49 A0AF C579 29BE` | -The full PGP key for this address, which is also available on several key servers (e.g. on [keys.gnupg.net](https://keys.gnupg.net)), is: +The full PGP key for this address, which is also available on several key servers (e.g. on [keyserver.ubuntu.com](https://keyserver.ubuntu.com)), is: ``` -----BEGIN PGP PUBLIC KEY BLOCK----- mQINBFUwGHYBEAC0wpGpBPkd8W1UdQjg9+cEFzeIEJRaoZoeuJD8mofwI5Ejnjdt diff --git a/cmake/RippledDocs.cmake b/cmake/RippledDocs.cmake index d93bc119c0..dda277bffa 100644 --- a/cmake/RippledDocs.cmake +++ b/cmake/RippledDocs.cmake @@ -53,9 +53,9 @@ set(download_script "${CMAKE_BINARY_DIR}/docs/download-cppreference.cmake") file(WRITE "${download_script}" "file(DOWNLOAD \ - http://upload.cppreference.com/mwiki/images/b/b2/html_book_20190607.zip \ + https://github.com/PeterFeicht/cppreference-doc/releases/download/v20250209/html-book-20250209.zip \ ${CMAKE_BINARY_DIR}/docs/cppreference.zip \ - EXPECTED_HASH MD5=82b3a612d7d35a83e3cb1195a63689ab \ + EXPECTED_HASH MD5=bda585f72fbca4b817b29a3d5746567b \ )\n \ execute_process( \ COMMAND \"${CMAKE_COMMAND}\" -E tar -xf cppreference.zip \ diff --git a/cmake/RippledSanity.cmake b/cmake/RippledSanity.cmake index 3dd5fb782f..28ce854135 100644 --- a/cmake/RippledSanity.cmake +++ b/cmake/RippledSanity.cmake @@ -2,16 +2,6 @@ convenience variables and sanity checks #]===================================================================] -include(ProcessorCount) - -if (NOT ep_procs) - ProcessorCount(ep_procs) - if (ep_procs GREATER 1) - # never use more than half of cores for EP builds - math (EXPR ep_procs "${ep_procs} / 2") - message (STATUS "Using ${ep_procs} cores for ExternalProject builds.") - endif () -endif () get_property(is_multiconfig GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) set (CMAKE_CONFIGURATION_TYPES "Debug;Release" CACHE STRING "" FORCE) diff --git a/cmake/RippledSettings.cmake b/cmake/RippledSettings.cmake index b2d7b0d9a5..9dc8609f58 100644 --- a/cmake/RippledSettings.cmake +++ b/cmake/RippledSettings.cmake @@ -18,7 +18,7 @@ if(tests) endif() endif() -option(unity "Creates a build using UNITY support in cmake. This is the default" ON) +option(unity "Creates a build using UNITY support in cmake." OFF) if(unity) if(NOT is_ci) set(CMAKE_UNITY_BUILD_BATCH_SIZE 15 CACHE STRING "") diff --git a/cmake/deps/Boost.cmake b/cmake/deps/Boost.cmake index 041c2380e1..031202f4d2 100644 --- a/cmake/deps/Boost.cmake +++ b/cmake/deps/Boost.cmake @@ -2,7 +2,6 @@ find_package(Boost 1.82 REQUIRED COMPONENTS chrono container - context coroutine date_time filesystem @@ -24,7 +23,7 @@ endif() target_link_libraries(ripple_boost INTERFACE - Boost::boost + Boost::headers Boost::chrono Boost::container Boost::coroutine diff --git a/cmake/xrpl_add_test.cmake b/cmake/xrpl_add_test.cmake new file mode 100644 index 0000000000..d61f4ece3d --- /dev/null +++ b/cmake/xrpl_add_test.cmake @@ -0,0 +1,41 @@ +include(isolate_headers) + +function(xrpl_add_test name) + set(target ${PROJECT_NAME}.test.${name}) + + file(GLOB_RECURSE sources CONFIGURE_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/${name}/*.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/${name}.cpp" + ) + add_executable(${target} EXCLUDE_FROM_ALL ${ARGN} ${sources}) + + isolate_headers( + ${target} + "${CMAKE_SOURCE_DIR}" + "${CMAKE_SOURCE_DIR}/tests/${name}" + PRIVATE + ) + + # Make sure the test isn't optimized away in unity builds + set_target_properties(${target} PROPERTIES + UNITY_BUILD_MODE GROUP + UNITY_BUILD_BATCH_SIZE 0) # Adjust as needed + + add_test(NAME ${target} COMMAND ${target}) + set_tests_properties( + ${target} PROPERTIES + FIXTURES_REQUIRED ${target}_fixture + ) + + add_test( + NAME ${target}.build + COMMAND + ${CMAKE_COMMAND} + --build ${CMAKE_BINARY_DIR} + --config $ + --target ${target} + ) + set_tests_properties(${target}.build PROPERTIES + FIXTURES_SETUP ${target}_fixture + ) +endfunction() diff --git a/conan/profiles/libxrpl b/conan/profiles/libxrpl new file mode 100644 index 0000000000..b037b8c4a2 --- /dev/null +++ b/conan/profiles/libxrpl @@ -0,0 +1,19 @@ +{% set os = detect_api.detect_os() %} +{% set arch = detect_api.detect_arch() %} +{% set compiler, version, compiler_exe = detect_api.detect_default_compiler() %} +{% set compiler_version = version %} +{% if os == "Linux" %} +{% set compiler_version = detect_api.default_compiler_version(compiler, version) %} +{% endif %} + +[settings] +os={{ os }} +arch={{ arch }} +compiler={{compiler}} +compiler.version={{ compiler_version }} +compiler.cppstd=20 +{% if os == "Windows" %} +compiler.runtime=static +{% else %} +compiler.libcxx={{detect_api.detect_libcxx(compiler, version, compiler_exe)}} +{% endif %} diff --git a/conanfile.py b/conanfile.py index a42c116ca2..d79b47bc6f 100644 --- a/conanfile.py +++ b/conanfile.py @@ -1,4 +1,4 @@ -from conan import ConanFile +from conan import ConanFile, __version__ as conan_version from conan.tools.cmake import CMake, CMakeToolchain, cmake_layout import re @@ -24,18 +24,20 @@ class Xrpl(ConanFile): } requires = [ - 'date/3.0.3', 'grpc/1.50.1', - 'libarchive/3.7.6', - 'nudb/2.0.8', - 'openssl/1.1.1v', + 'libarchive/3.8.1', + 'nudb/2.0.9', + 'openssl/1.1.1w', 'soci/4.0.3', - 'xxhash/0.8.2', 'zlib/1.3.1', ] + test_requires = [ + 'doctest/2.4.11', + ] + tool_requires = [ - 'protobuf/3.21.9', + 'protobuf/3.21.12', ] default_options = { @@ -87,26 +89,31 @@ class Xrpl(ConanFile): } def set_version(self): - path = f'{self.recipe_folder}/src/libxrpl/protocol/BuildInfo.cpp' - regex = r'versionString\s?=\s?\"(.*)\"' - with open(path, 'r') as file: - matches = (re.search(regex, line) for line in file) - match = next(m for m in matches if m) - self.version = match.group(1) + if self.version is None: + path = f'{self.recipe_folder}/src/libxrpl/protocol/BuildInfo.cpp' + regex = r'versionString\s?=\s?\"(.*)\"' + with open(path, encoding='utf-8') as file: + matches = (re.search(regex, line) for line in file) + match = next(m for m in matches if m) + self.version = match.group(1) def configure(self): if self.settings.compiler == 'apple-clang': self.options['boost'].visibility = 'global' def requirements(self): - self.requires('boost/1.83.0', force=True) + # Conan 2 requires transitive headers to be specified + transitive_headers_opt = {'transitive_headers': True} if conan_version.split('.')[0] == '2' else {} + self.requires('boost/1.83.0', force=True, **transitive_headers_opt) + self.requires('date/3.0.4', **transitive_headers_opt) self.requires('lz4/1.10.0', force=True) - self.requires('protobuf/3.21.9', force=True) - self.requires('sqlite3/3.47.0', force=True) + self.requires('protobuf/3.21.12', force=True) + self.requires('sqlite3/3.49.1', force=True) if self.options.jemalloc: self.requires('jemalloc/5.3.0') if self.options.rocksdb: self.requires('rocksdb/9.7.3') + self.requires('xxhash/0.8.3', **transitive_headers_opt) exports_sources = ( 'CMakeLists.txt', @@ -136,6 +143,8 @@ class Xrpl(ConanFile): tc.variables['static'] = self.options.static tc.variables['unity'] = self.options.unity tc.variables['xrpld'] = self.options.xrpld + if self.settings.compiler == 'clang' and self.settings.compiler.version == 16: + tc.extra_cxxflags = ["-DBOOST_ASIO_DISABLE_CONCEPTS"] tc.generate() def build(self): @@ -161,7 +170,17 @@ class Xrpl(ConanFile): # `include/`, not `include/ripple/proto/`. libxrpl.includedirs = ['include', 'include/ripple/proto'] libxrpl.requires = [ - 'boost::boost', + 'boost::headers', + 'boost::chrono', + 'boost::container', + 'boost::coroutine', + 'boost::date_time', + 'boost::filesystem', + 'boost::json', + 'boost::program_options', + 'boost::regex', + 'boost::system', + 'boost::thread', 'date::date', 'grpc::grpc++', 'libarchive::libarchive', diff --git a/docs/build/environment.md b/docs/build/environment.md index 7fe89ffb49..760be144d8 100644 --- a/docs/build/environment.md +++ b/docs/build/environment.md @@ -23,7 +23,7 @@ direction. ``` apt update -apt install --yes curl git libssl-dev python3.10-dev python3-pip make g++-11 libprotobuf-dev protobuf-compiler +apt install --yes curl git libssl-dev pipx python3.10-dev python3-pip make g++-11 libprotobuf-dev protobuf-compiler curl --location --remote-name \ "https://github.com/Kitware/CMake/releases/download/v3.25.1/cmake-3.25.1.tar.gz" @@ -35,7 +35,8 @@ make --jobs $(nproc) make install cd .. -pip3 install 'conan<2' +pipx install 'conan<2' +pipx ensurepath ``` [1]: https://github.com/thejohnfreeman/rippled-docker/blob/master/ubuntu-22.04/install.sh diff --git a/external/antithesis-sdk/CMakeLists.txt b/external/antithesis-sdk/CMakeLists.txt index d2c1f536af..46c7b4bf7a 100644 --- a/external/antithesis-sdk/CMakeLists.txt +++ b/external/antithesis-sdk/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.25) +cmake_minimum_required(VERSION 3.18) # Note, version set explicitly by rippled project project(antithesis-sdk-cpp VERSION 0.4.4 LANGUAGES CXX) diff --git a/external/nudb/conandata.yml b/external/nudb/conandata.yml deleted file mode 100644 index 721129f88e..0000000000 --- a/external/nudb/conandata.yml +++ /dev/null @@ -1,10 +0,0 @@ -sources: - "2.0.8": - url: "https://github.com/CPPAlliance/NuDB/archive/2.0.8.tar.gz" - sha256: "9b71903d8ba111cd893ab064b9a8b6ac4124ed8bd6b4f67250205bc43c7f13a8" -patches: - "2.0.8": - - patch_file: "patches/2.0.8-0001-add-include-stdexcept-for-msvc.patch" - patch_description: "Fix build for MSVC by including stdexcept" - patch_type: "portability" - patch_source: "https://github.com/cppalliance/NuDB/pull/100/files" diff --git a/external/nudb/conanfile.py b/external/nudb/conanfile.py deleted file mode 100644 index a046e2ba89..0000000000 --- a/external/nudb/conanfile.py +++ /dev/null @@ -1,72 +0,0 @@ -import os - -from conan import ConanFile -from conan.tools.build import check_min_cppstd -from conan.tools.files import apply_conandata_patches, copy, export_conandata_patches, get -from conan.tools.layout import basic_layout - -required_conan_version = ">=1.52.0" - - -class NudbConan(ConanFile): - name = "nudb" - description = "A fast key/value insert-only database for SSD drives in C++11" - license = "BSL-1.0" - url = "https://github.com/conan-io/conan-center-index" - homepage = "https://github.com/CPPAlliance/NuDB" - topics = ("header-only", "KVS", "insert-only") - - package_type = "header-library" - settings = "os", "arch", "compiler", "build_type" - no_copy_source = True - - @property - def _min_cppstd(self): - return 11 - - def export_sources(self): - export_conandata_patches(self) - - def layout(self): - basic_layout(self, src_folder="src") - - def requirements(self): - self.requires("boost/1.83.0") - - def package_id(self): - self.info.clear() - - def validate(self): - if self.settings.compiler.cppstd: - check_min_cppstd(self, self._min_cppstd) - - def source(self): - get(self, **self.conan_data["sources"][self.version], strip_root=True) - - def build(self): - apply_conandata_patches(self) - - def package(self): - copy(self, "LICENSE*", - dst=os.path.join(self.package_folder, "licenses"), - src=self.source_folder) - copy(self, "*", - dst=os.path.join(self.package_folder, "include"), - src=os.path.join(self.source_folder, "include")) - - def package_info(self): - self.cpp_info.bindirs = [] - self.cpp_info.libdirs = [] - - self.cpp_info.set_property("cmake_target_name", "NuDB") - self.cpp_info.set_property("cmake_target_aliases", ["NuDB::nudb"]) - self.cpp_info.set_property("cmake_find_mode", "both") - - self.cpp_info.components["core"].set_property("cmake_target_name", "nudb") - self.cpp_info.components["core"].names["cmake_find_package"] = "nudb" - self.cpp_info.components["core"].names["cmake_find_package_multi"] = "nudb" - self.cpp_info.components["core"].requires = ["boost::thread", "boost::system"] - - # TODO: to remove in conan v2 once cmake_find_package_* generators removed - self.cpp_info.names["cmake_find_package"] = "NuDB" - self.cpp_info.names["cmake_find_package_multi"] = "NuDB" diff --git a/external/nudb/patches/2.0.8-0001-add-include-stdexcept-for-msvc.patch b/external/nudb/patches/2.0.8-0001-add-include-stdexcept-for-msvc.patch deleted file mode 100644 index 2d5264f3ce..0000000000 --- a/external/nudb/patches/2.0.8-0001-add-include-stdexcept-for-msvc.patch +++ /dev/null @@ -1,24 +0,0 @@ -diff --git a/include/nudb/detail/stream.hpp b/include/nudb/detail/stream.hpp -index 6c07bf1..e0ce8ed 100644 ---- a/include/nudb/detail/stream.hpp -+++ b/include/nudb/detail/stream.hpp -@@ -14,6 +14,7 @@ - #include - #include - #include -+#include - - namespace nudb { - namespace detail { -diff --git a/include/nudb/impl/context.ipp b/include/nudb/impl/context.ipp -index beb7058..ffde0b3 100644 ---- a/include/nudb/impl/context.ipp -+++ b/include/nudb/impl/context.ipp -@@ -9,6 +9,7 @@ - #define NUDB_IMPL_CONTEXT_IPP - - #include -+#include - - namespace nudb { - diff --git a/include/xrpl/basics/Buffer.h b/include/xrpl/basics/Buffer.h index b2f1163452..3379a923f0 100644 --- a/include/xrpl/basics/Buffer.h +++ b/include/xrpl/basics/Buffer.h @@ -25,6 +25,7 @@ #include #include +#include namespace ripple { diff --git a/include/xrpl/basics/Expected.h b/include/xrpl/basics/Expected.h index 9afb160d9d..d2440f63ab 100644 --- a/include/xrpl/basics/Expected.h +++ b/include/xrpl/basics/Expected.h @@ -22,8 +22,18 @@ #include +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated" +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +#endif + #include +#if defined(__clang__) +#pragma clang diagnostic pop +#endif + #include namespace ripple { diff --git a/include/xrpl/basics/StringUtilities.h b/include/xrpl/basics/StringUtilities.h index 23d60e2db4..5f905638cb 100644 --- a/include/xrpl/basics/StringUtilities.h +++ b/include/xrpl/basics/StringUtilities.h @@ -29,7 +29,6 @@ #include #include #include -#include #include namespace ripple { diff --git a/include/xrpl/basics/algorithm.h b/include/xrpl/basics/algorithm.h index ed6e8080d9..673d5e955b 100644 --- a/include/xrpl/basics/algorithm.h +++ b/include/xrpl/basics/algorithm.h @@ -20,7 +20,6 @@ #ifndef RIPPLE_ALGORITHM_H_INCLUDED #define RIPPLE_ALGORITHM_H_INCLUDED -#include #include namespace ripple { diff --git a/include/xrpl/basics/hardened_hash.h b/include/xrpl/basics/hardened_hash.h index 0b77b0a07a..aae6c55dff 100644 --- a/include/xrpl/basics/hardened_hash.h +++ b/include/xrpl/basics/hardened_hash.h @@ -24,12 +24,8 @@ #include #include -#include #include #include -#include -#include -#include #include namespace ripple { diff --git a/include/xrpl/basics/mulDiv.h b/include/xrpl/basics/mulDiv.h index e338f87c81..96d466f6c7 100644 --- a/include/xrpl/basics/mulDiv.h +++ b/include/xrpl/basics/mulDiv.h @@ -23,7 +23,6 @@ #include #include #include -#include namespace ripple { auto constexpr muldiv_max = std::numeric_limits::max(); diff --git a/include/xrpl/basics/tagged_integer.h b/include/xrpl/basics/tagged_integer.h index 471fa8eb1e..ed30b6f120 100644 --- a/include/xrpl/basics/tagged_integer.h +++ b/include/xrpl/basics/tagged_integer.h @@ -24,10 +24,8 @@ #include -#include #include #include -#include namespace ripple { diff --git a/include/xrpl/beast/clock/abstract_clock.h b/include/xrpl/beast/clock/abstract_clock.h index 128ab82b4b..7b0f04225f 100644 --- a/include/xrpl/beast/clock/abstract_clock.h +++ b/include/xrpl/beast/clock/abstract_clock.h @@ -20,9 +20,6 @@ #ifndef BEAST_CHRONO_ABSTRACT_CLOCK_H_INCLUDED #define BEAST_CHRONO_ABSTRACT_CLOCK_H_INCLUDED -#include -#include - namespace beast { /** Abstract interface to a clock. diff --git a/include/xrpl/beast/clock/manual_clock.h b/include/xrpl/beast/clock/manual_clock.h index 32ff76bb07..a0e82b7014 100644 --- a/include/xrpl/beast/clock/manual_clock.h +++ b/include/xrpl/beast/clock/manual_clock.h @@ -23,6 +23,8 @@ #include #include +#include + namespace beast { /** Manual clock implementation. diff --git a/include/xrpl/beast/container/aged_container_utility.h b/include/xrpl/beast/container/aged_container_utility.h index b64cefbf5a..d315f05346 100644 --- a/include/xrpl/beast/container/aged_container_utility.h +++ b/include/xrpl/beast/container/aged_container_utility.h @@ -22,6 +22,7 @@ #include +#include #include namespace beast { diff --git a/include/xrpl/beast/container/detail/aged_associative_container.h b/include/xrpl/beast/container/detail/aged_associative_container.h index 5ff7901552..678fbe4e17 100644 --- a/include/xrpl/beast/container/detail/aged_associative_container.h +++ b/include/xrpl/beast/container/detail/aged_associative_container.h @@ -20,8 +20,6 @@ #ifndef BEAST_CONTAINER_DETAIL_AGED_ASSOCIATIVE_CONTAINER_H_INCLUDED #define BEAST_CONTAINER_DETAIL_AGED_ASSOCIATIVE_CONTAINER_H_INCLUDED -#include - namespace beast { namespace detail { diff --git a/include/xrpl/beast/container/detail/aged_ordered_container.h b/include/xrpl/beast/container/detail/aged_ordered_container.h index 8c978d0517..ef3e1b5ea1 100644 --- a/include/xrpl/beast/container/detail/aged_ordered_container.h +++ b/include/xrpl/beast/container/detail/aged_ordered_container.h @@ -33,7 +33,6 @@ #include #include #include -#include #include #include #include diff --git a/include/xrpl/beast/core/LexicalCast.h b/include/xrpl/beast/core/LexicalCast.h index aa67bcad50..5551e1f2dc 100644 --- a/include/xrpl/beast/core/LexicalCast.h +++ b/include/xrpl/beast/core/LexicalCast.h @@ -29,11 +29,9 @@ #include #include #include -#include #include #include #include -#include namespace beast { diff --git a/include/xrpl/beast/hash/hash_append.h b/include/xrpl/beast/hash/hash_append.h index 6b11fe1eb3..e113567ab1 100644 --- a/include/xrpl/beast/hash/hash_append.h +++ b/include/xrpl/beast/hash/hash_append.h @@ -24,14 +24,36 @@ #include #include +/* + +Workaround for overzealous clang warning, which trips on libstdc++ headers + + In file included from + /usr/lib/gcc/x86_64-linux-gnu/12/../../../../include/c++/12/bits/stl_algo.h:61: + /usr/lib/gcc/x86_64-linux-gnu/12/../../../../include/c++/12/bits/stl_tempbuf.h:263:8: + error: 'get_temporary_buffer> *>>' is deprecated + [-Werror,-Wdeprecated-declarations] 263 | + std::get_temporary_buffer(_M_original_len)); + ^ +*/ + +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated" +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +#endif + +#include + +#if defined(__clang__) +#pragma clang diagnostic pop +#endif + #include #include -#include #include -#include -#include #include -#include #include #include #include diff --git a/include/xrpl/beast/hash/uhash.h b/include/xrpl/beast/hash/uhash.h index ab3eaad039..ac4ba7256d 100644 --- a/include/xrpl/beast/hash/uhash.h +++ b/include/xrpl/beast/hash/uhash.h @@ -30,7 +30,7 @@ namespace beast { template struct uhash { - explicit uhash() = default; + uhash() = default; using result_type = typename Hasher::result_type; diff --git a/include/xrpl/beast/net/IPAddress.h b/include/xrpl/beast/net/IPAddress.h index 62469cfda1..fb5dac90ec 100644 --- a/include/xrpl/beast/net/IPAddress.h +++ b/include/xrpl/beast/net/IPAddress.h @@ -29,11 +29,7 @@ #include #include -#include -#include -#include #include -#include //------------------------------------------------------------------------------ diff --git a/include/xrpl/beast/net/IPAddressV4.h b/include/xrpl/beast/net/IPAddressV4.h index 98a92dba20..c65adae05b 100644 --- a/include/xrpl/beast/net/IPAddressV4.h +++ b/include/xrpl/beast/net/IPAddressV4.h @@ -24,12 +24,6 @@ #include -#include -#include -#include -#include -#include - namespace beast { namespace IP { diff --git a/include/xrpl/beast/net/IPAddressV6.h b/include/xrpl/beast/net/IPAddressV6.h index 4a4ef73b86..9e24b228e5 100644 --- a/include/xrpl/beast/net/IPAddressV6.h +++ b/include/xrpl/beast/net/IPAddressV6.h @@ -24,12 +24,6 @@ #include -#include -#include -#include -#include -#include - namespace beast { namespace IP { diff --git a/include/xrpl/beast/net/IPEndpoint.h b/include/xrpl/beast/net/IPEndpoint.h index e66e7f4caa..8d43eb0ba9 100644 --- a/include/xrpl/beast/net/IPEndpoint.h +++ b/include/xrpl/beast/net/IPEndpoint.h @@ -25,7 +25,6 @@ #include #include -#include #include #include @@ -215,7 +214,7 @@ namespace std { template <> struct hash<::beast::IP::Endpoint> { - explicit hash() = default; + hash() = default; std::size_t operator()(::beast::IP::Endpoint const& endpoint) const @@ -230,7 +229,7 @@ namespace boost { template <> struct hash<::beast::IP::Endpoint> { - explicit hash() = default; + hash() = default; std::size_t operator()(::beast::IP::Endpoint const& endpoint) const diff --git a/include/xrpl/beast/rfc2616.h b/include/xrpl/beast/rfc2616.h index 648fbc22e2..d6b3fa3cda 100644 --- a/include/xrpl/beast/rfc2616.h +++ b/include/xrpl/beast/rfc2616.h @@ -28,10 +28,8 @@ #include #include -#include #include #include -#include #include namespace beast { diff --git a/include/xrpl/beast/test/yield_to.h b/include/xrpl/beast/test/yield_to.h index 9e9f83b897..27a3a2db20 100644 --- a/include/xrpl/beast/test/yield_to.h +++ b/include/xrpl/beast/test/yield_to.h @@ -13,7 +13,6 @@ #include #include -#include #include #include #include diff --git a/include/xrpl/beast/unit_test/reporter.h b/include/xrpl/beast/unit_test/reporter.h index e7a7d4b3ad..0054daab98 100644 --- a/include/xrpl/beast/unit_test/reporter.h +++ b/include/xrpl/beast/unit_test/reporter.h @@ -16,7 +16,6 @@ #include #include -#include #include #include #include diff --git a/include/xrpl/beast/unit_test/runner.h b/include/xrpl/beast/unit_test/runner.h index 283f7c8723..977cc45035 100644 --- a/include/xrpl/beast/unit_test/runner.h +++ b/include/xrpl/beast/unit_test/runner.h @@ -13,7 +13,6 @@ #include #include -#include #include namespace beast { diff --git a/include/xrpl/beast/utility/rngfill.h b/include/xrpl/beast/utility/rngfill.h index 0188e5c529..2b5a9ba040 100644 --- a/include/xrpl/beast/utility/rngfill.h +++ b/include/xrpl/beast/utility/rngfill.h @@ -48,8 +48,10 @@ rngfill(void* buffer, std::size_t bytes, Generator& g) #ifdef __GNUC__ // gcc 11.1 (falsely) warns about an array-bounds overflow in release mode. + // gcc 12.1 (also falsely) warns about an string overflow in release mode. #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Warray-bounds" +#pragma GCC diagnostic ignored "-Wstringop-overflow" #endif if (bytes > 0) diff --git a/include/xrpl/json/json_value.h b/include/xrpl/json/json_value.h index 2e815b79f2..272d12d680 100644 --- a/include/xrpl/json/json_value.h +++ b/include/xrpl/json/json_value.h @@ -26,7 +26,6 @@ #include #include #include -#include #include /** \brief JSON (JavaScript Object Notation). diff --git a/include/xrpl/protocol/AccountID.h b/include/xrpl/protocol/AccountID.h index 2677dd76bc..d546346bb4 100644 --- a/include/xrpl/protocol/AccountID.h +++ b/include/xrpl/protocol/AccountID.h @@ -29,7 +29,6 @@ #include #include -#include #include #include @@ -149,7 +148,7 @@ namespace std { template <> struct hash : ripple::AccountID::hasher { - explicit hash() = default; + hash() = default; }; } // namespace std diff --git a/include/xrpl/protocol/ApiVersion.h b/include/xrpl/protocol/ApiVersion.h index dd09cf6bd1..deafafa513 100644 --- a/include/xrpl/protocol/ApiVersion.h +++ b/include/xrpl/protocol/ApiVersion.h @@ -20,7 +20,6 @@ #ifndef RIPPLE_PROTOCOL_APIVERSION_H_INCLUDED #define RIPPLE_PROTOCOL_APIVERSION_H_INCLUDED -#include #include #include diff --git a/include/xrpl/protocol/Batch.h b/include/xrpl/protocol/Batch.h new file mode 100644 index 0000000000..1388bbd2f1 --- /dev/null +++ b/include/xrpl/protocol/Batch.h @@ -0,0 +1,37 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include + +namespace ripple { + +inline void +serializeBatch( + Serializer& msg, + std::uint32_t const& flags, + std::vector const& txids) +{ + msg.add32(HashPrefix::batch); + msg.add32(flags); + msg.add32(std::uint32_t(txids.size())); + for (auto const& txid : txids) + msg.addBitString(txid); +} + +} // namespace ripple \ No newline at end of file diff --git a/include/xrpl/protocol/Book.h b/include/xrpl/protocol/Book.h index 0a04deb277..a8b9afacac 100644 --- a/include/xrpl/protocol/Book.h +++ b/include/xrpl/protocol/Book.h @@ -21,6 +21,7 @@ #define RIPPLE_PROTOCOL_BOOK_H_INCLUDED #include +#include #include #include @@ -36,12 +37,17 @@ class Book final : public CountedObject public: Issue in; Issue out; + std::optional domain; Book() { } - Book(Issue const& in_, Issue const& out_) : in(in_), out(out_) + Book( + Issue const& in_, + Issue const& out_, + std::optional const& domain_) + : in(in_), out(out_), domain(domain_) { } }; @@ -61,6 +67,8 @@ hash_append(Hasher& h, Book const& b) { using beast::hash_append; hash_append(h, b.in, b.out); + if (b.domain) + hash_append(h, *(b.domain)); } Book @@ -71,7 +79,8 @@ reversed(Book const& book); [[nodiscard]] inline constexpr bool operator==(Book const& lhs, Book const& rhs) { - return (lhs.in == rhs.in) && (lhs.out == rhs.out); + return (lhs.in == rhs.in) && (lhs.out == rhs.out) && + (lhs.domain == rhs.domain); } /** @} */ @@ -82,7 +91,18 @@ operator<=>(Book const& lhs, Book const& rhs) { if (auto const c{lhs.in <=> rhs.in}; c != 0) return c; - return lhs.out <=> rhs.out; + if (auto const c{lhs.out <=> rhs.out}; c != 0) + return c; + + // Manually compare optionals + if (lhs.domain && rhs.domain) + return *lhs.domain <=> *rhs.domain; // Compare values if both exist + if (!lhs.domain && rhs.domain) + return std::weak_ordering::less; // Empty is considered less + if (lhs.domain && !rhs.domain) + return std::weak_ordering::greater; // Non-empty is greater + + return std::weak_ordering::equivalent; // Both are empty } /** @} */ @@ -104,7 +124,7 @@ private: boost::base_from_member, 1>; public: - explicit hash() = default; + hash() = default; using value_type = std::size_t; using argument_type = ripple::Issue; @@ -126,12 +146,14 @@ template <> struct hash { private: - using hasher = std::hash; + using issue_hasher = std::hash; + using uint256_hasher = ripple::uint256::hasher; - hasher m_hasher; + issue_hasher m_issue_hasher; + uint256_hasher m_uint256_hasher; public: - explicit hash() = default; + hash() = default; using value_type = std::size_t; using argument_type = ripple::Book; @@ -139,8 +161,12 @@ public: value_type operator()(argument_type const& value) const { - value_type result(m_hasher(value.in)); - boost::hash_combine(result, m_hasher(value.out)); + value_type result(m_issue_hasher(value.in)); + boost::hash_combine(result, m_issue_hasher(value.out)); + + if (value.domain) + boost::hash_combine(result, m_uint256_hasher(*value.domain)); + return result; } }; @@ -154,7 +180,7 @@ namespace boost { template <> struct hash : std::hash { - explicit hash() = default; + hash() = default; using Base = std::hash; // VFALCO NOTE broken in vs2012 @@ -164,7 +190,7 @@ struct hash : std::hash template <> struct hash : std::hash { - explicit hash() = default; + hash() = default; using Base = std::hash; // VFALCO NOTE broken in vs2012 diff --git a/include/xrpl/protocol/ErrorCodes.h b/include/xrpl/protocol/ErrorCodes.h index 66b4dd178c..f06b927566 100644 --- a/include/xrpl/protocol/ErrorCodes.h +++ b/include/xrpl/protocol/ErrorCodes.h @@ -154,7 +154,10 @@ enum error_code_i { // Simulate rpcTX_SIGNED = 96, - rpcLAST = rpcTX_SIGNED // rpcLAST should always equal the last code. + // Pathfinding + rpcDOMAIN_MALFORMED = 97, + + rpcLAST = rpcDOMAIN_MALFORMED // rpcLAST should always equal the last code. }; /** Codes returned in the `warnings` array of certain RPC commands. @@ -166,6 +169,8 @@ enum warning_code_i { warnRPC_AMENDMENT_BLOCKED = 1002, warnRPC_EXPIRED_VALIDATOR_LIST = 1003, // unused = 1004 + warnRPC_FIELDS_DEPRECATED = 2004, // rippled needs to maintain + // compatibility with Clio on this code. }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/Feature.h b/include/xrpl/protocol/Feature.h index a2eb7d8e50..c55776a5ce 100644 --- a/include/xrpl/protocol/Feature.h +++ b/include/xrpl/protocol/Feature.h @@ -24,7 +24,6 @@ #include -#include #include #include #include @@ -55,6 +54,18 @@ * `VoteBehavior::DefaultYes`. The communication process is beyond * the scope of these instructions. * + * 5) A feature marked as Obsolete can mean either: + * 1) It is in the ledger (marked as Supported::yes) and it is on its way to + * become Retired + * 2) The feature is not in the ledger (has always been marked as + * Supported::no) and the code to support it has been removed + * + * If we want to discontinue a feature that we've never fully supported and + * the feature has never been enabled, we should remove all the related + * code, and mark the feature as "abandoned". To do this: + * + * 1) Open features.macro, move the feature to the abandoned section and + * change the macro to XRPL_ABANDON * * When a feature has been enabled for several years, the conditional code * may be removed, and the feature "retired". To retire a feature: @@ -88,10 +99,13 @@ namespace detail { #undef XRPL_FIX #pragma push_macro("XRPL_RETIRE") #undef XRPL_RETIRE +#pragma push_macro("XRPL_ABANDON") +#undef XRPL_ABANDON #define XRPL_FEATURE(name, supported, vote) +1 #define XRPL_FIX(name, supported, vote) +1 #define XRPL_RETIRE(name) +1 +#define XRPL_ABANDON(name) +1 // This value SHOULD be equal to the number of amendments registered in // Feature.cpp. Because it's only used to reserve storage, and determine how @@ -108,6 +122,8 @@ static constexpr std::size_t numFeatures = #pragma pop_macro("XRPL_FIX") #undef XRPL_FEATURE #pragma pop_macro("XRPL_FEATURE") +#undef XRPL_ABANDON +#pragma pop_macro("XRPL_ABANDON") /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated @@ -349,10 +365,13 @@ foreachFeature(FeatureBitset bs, F&& f) #undef XRPL_FIX #pragma push_macro("XRPL_RETIRE") #undef XRPL_RETIRE +#pragma push_macro("XRPL_ABANDON") +#undef XRPL_ABANDON #define XRPL_FEATURE(name, supported, vote) extern uint256 const feature##name; #define XRPL_FIX(name, supported, vote) extern uint256 const fix##name; #define XRPL_RETIRE(name) +#define XRPL_ABANDON(name) #include @@ -362,6 +381,8 @@ foreachFeature(FeatureBitset bs, F&& f) #pragma pop_macro("XRPL_FIX") #undef XRPL_FEATURE #pragma pop_macro("XRPL_FEATURE") +#undef XRPL_ABANDON +#pragma pop_macro("XRPL_ABANDON") } // namespace ripple diff --git a/include/xrpl/protocol/HashPrefix.h b/include/xrpl/protocol/HashPrefix.h index ab825658e8..7e486af4c0 100644 --- a/include/xrpl/protocol/HashPrefix.h +++ b/include/xrpl/protocol/HashPrefix.h @@ -88,6 +88,9 @@ enum class HashPrefix : std::uint32_t { /** Credentials signature */ credential = detail::make_hash_prefix('C', 'R', 'D'), + + /** Batch */ + batch = detail::make_hash_prefix('B', 'C', 'H'), }; template diff --git a/include/xrpl/protocol/IOUAmount.h b/include/xrpl/protocol/IOUAmount.h index a27069e37b..93fba4150d 100644 --- a/include/xrpl/protocol/IOUAmount.h +++ b/include/xrpl/protocol/IOUAmount.h @@ -98,6 +98,12 @@ public: static IOUAmount minPositiveAmount(); + + friend std::ostream& + operator<<(std::ostream& os, IOUAmount const& x) + { + return os << to_string(x); + } }; inline IOUAmount::IOUAmount(beast::Zero) diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index 57c8727ae6..3e3f2843c1 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -32,6 +32,7 @@ #include #include +#include namespace ripple { diff --git a/include/xrpl/protocol/Issue.h b/include/xrpl/protocol/Issue.h index 83ef337c35..eb4861f59b 100644 --- a/include/xrpl/protocol/Issue.h +++ b/include/xrpl/protocol/Issue.h @@ -24,9 +24,6 @@ #include #include -#include -#include - namespace ripple { /** A currency issued by an account. diff --git a/include/xrpl/protocol/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h index 3edd656213..e3efe8fec2 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -145,13 +145,15 @@ enum LedgerSpecificFlags { 0x10000000, // True, reject new paychans lsfDisallowIncomingTrustline = 0x20000000, // True, reject new trustlines (only if no issued assets) - // 0x40000000 is available + lsfAllowTrustLineLocking = + 0x40000000, // True, enable trustline locking lsfAllowTrustLineClawback = 0x80000000, // True, enable clawback // ltOFFER lsfPassive = 0x00010000, lsfSell = 0x00020000, // True, offer was placed as a sell. + lsfHybrid = 0x00040000, // True, offer is hybrid. // ltRIPPLE_STATE lsfLowReserve = 0x00010000, // True, if entry counts toward reserve. diff --git a/include/xrpl/protocol/MultiApiJson.h b/include/xrpl/protocol/MultiApiJson.h index 1e35bdbda2..4a3d0115de 100644 --- a/include/xrpl/protocol/MultiApiJson.h +++ b/include/xrpl/protocol/MultiApiJson.h @@ -28,7 +28,6 @@ #include #include #include -#include #include #include diff --git a/include/xrpl/protocol/NFTSyntheticSerializer.h b/include/xrpl/protocol/NFTSyntheticSerializer.h index e57b3ff71c..cb33744485 100644 --- a/include/xrpl/protocol/NFTSyntheticSerializer.h +++ b/include/xrpl/protocol/NFTSyntheticSerializer.h @@ -28,6 +28,8 @@ namespace ripple { +namespace RPC { + /** Adds common synthetic fields to transaction-related JSON responses @@ -40,6 +42,7 @@ insertNFTSyntheticInJson( TxMeta const&); /** @} */ +} // namespace RPC } // namespace ripple #endif diff --git a/include/xrpl/protocol/Permissions.h b/include/xrpl/protocol/Permissions.h index 8ba53d94d7..67f3eea8d7 100644 --- a/include/xrpl/protocol/Permissions.h +++ b/include/xrpl/protocol/Permissions.h @@ -25,7 +25,6 @@ #include #include #include -#include namespace ripple { /** diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index 1a49d9d09e..0599a9e7dd 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -171,6 +171,9 @@ std::size_t constexpr maxTrim = 25; */ std::size_t constexpr permissionMaxSize = 10; +/** The maximum number of transactions that can be in a batch. */ +std::size_t constexpr maxBatchTxCount = 8; + } // namespace ripple #endif diff --git a/include/xrpl/protocol/PublicKey.h b/include/xrpl/protocol/PublicKey.h index c68656877c..9bf01e5cda 100644 --- a/include/xrpl/protocol/PublicKey.h +++ b/include/xrpl/protocol/PublicKey.h @@ -32,7 +32,6 @@ #include #include #include -#include namespace ripple { diff --git a/include/xrpl/protocol/Rules.h b/include/xrpl/protocol/Rules.h index 6b22d01afe..efdaf803fd 100644 --- a/include/xrpl/protocol/Rules.h +++ b/include/xrpl/protocol/Rules.h @@ -28,6 +28,9 @@ namespace ripple { +bool +isFeatureEnabled(uint256 const& feature); + class DigestAwareReadView; /** Rules controlling protocol behavior. */ diff --git a/include/xrpl/protocol/SOTemplate.h b/include/xrpl/protocol/SOTemplate.h index 9fd4cbf19d..14497b4222 100644 --- a/include/xrpl/protocol/SOTemplate.h +++ b/include/xrpl/protocol/SOTemplate.h @@ -25,7 +25,6 @@ #include #include -#include #include namespace ripple { diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index c66d273254..f1e34463b6 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -703,6 +703,12 @@ isXRP(STAmount const& amount) return amount.native(); } +bool +canAdd(STAmount const& amt1, STAmount const& amt2); + +bool +canSubtract(STAmount const& amt1, STAmount const& amt2); + // Since `canonicalize` does not have access to a ledger, this is needed to put // the low-level routine stAmountCanonicalize on an amendment switch. Only // transactions need to use this switchover. Outside of a transaction it's safe diff --git a/include/xrpl/protocol/STBase.h b/include/xrpl/protocol/STBase.h index eec9a97987..3f5a3b57ab 100644 --- a/include/xrpl/protocol/STBase.h +++ b/include/xrpl/protocol/STBase.h @@ -24,7 +24,6 @@ #include #include -#include #include #include #include diff --git a/include/xrpl/protocol/STBlob.h b/include/xrpl/protocol/STBlob.h index 80832b2688..374abd2a7c 100644 --- a/include/xrpl/protocol/STBlob.h +++ b/include/xrpl/protocol/STBlob.h @@ -27,7 +27,6 @@ #include #include -#include namespace ripple { diff --git a/include/xrpl/protocol/STTx.h b/include/xrpl/protocol/STTx.h index b00495bf76..f0d2157283 100644 --- a/include/xrpl/protocol/STTx.h +++ b/include/xrpl/protocol/STTx.h @@ -125,10 +125,16 @@ public: @return `true` if valid signature. If invalid, the error message string. */ enum class RequireFullyCanonicalSig : bool { no, yes }; + Expected checkSign(RequireFullyCanonicalSig requireCanonicalSig, Rules const& rules) const; + Expected + checkBatchSign( + RequireFullyCanonicalSig requireCanonicalSig, + Rules const& rules) const; + // SQL Functions with metadata. static std::string const& getMetaSQLInsertReplaceHeader(); @@ -144,6 +150,9 @@ public: char status, std::string const& escapedMetaData) const; + std::vector + getBatchTransactionIDs() const; + private: Expected checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const; @@ -153,12 +162,24 @@ private: RequireFullyCanonicalSig requireCanonicalSig, Rules const& rules) const; + Expected + checkBatchSingleSign( + STObject const& batchSigner, + RequireFullyCanonicalSig requireCanonicalSig) const; + + Expected + checkBatchMultiSign( + STObject const& batchSigner, + RequireFullyCanonicalSig requireCanonicalSig, + Rules const& rules) const; + STBase* copy(std::size_t n, void* buf) const override; STBase* move(std::size_t n, void* buf) override; friend class detail::STVar; + mutable std::vector batch_txn_ids_; }; bool diff --git a/include/xrpl/protocol/STValidation.h b/include/xrpl/protocol/STValidation.h index f87923c940..991922514d 100644 --- a/include/xrpl/protocol/STValidation.h +++ b/include/xrpl/protocol/STValidation.h @@ -28,8 +28,6 @@ #include #include -#include -#include #include #include diff --git a/include/xrpl/protocol/Serializer.h b/include/xrpl/protocol/Serializer.h index 9c77aa4111..5ea4d3ca96 100644 --- a/include/xrpl/protocol/Serializer.h +++ b/include/xrpl/protocol/Serializer.h @@ -33,7 +33,6 @@ #include #include -#include #include namespace ripple { diff --git a/include/xrpl/protocol/Sign.h b/include/xrpl/protocol/Sign.h index 7e1156ceda..5aa9fabddc 100644 --- a/include/xrpl/protocol/Sign.h +++ b/include/xrpl/protocol/Sign.h @@ -25,8 +25,6 @@ #include #include -#include - namespace ripple { /** Sign an STObject diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index b87bc3f8a4..9ace6b80f8 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -139,8 +139,8 @@ enum TEMcodes : TERUnderlyingType { temARRAY_EMPTY, temARRAY_TOO_LARGE, - temBAD_TRANSFER_FEE, + temINVALID_INNER_BATCH, }; //------------------------------------------------------------------------------ @@ -360,6 +360,8 @@ enum TECcodes : TERUnderlyingType { tecWRONG_ASSET = 194, tecLIMIT_EXCEEDED = 195, tecPSEUDO_ACCOUNT = 196, + tecPRECISION_LOSS = 197, + tecNO_DELEGATE_PERMISSION = 198, }; //------------------------------------------------------------------------------ @@ -645,37 +647,37 @@ using TER = TERSubset; //------------------------------------------------------------------------------ inline bool -isTelLocal(TER x) +isTelLocal(TER x) noexcept { - return ((x) >= telLOCAL_ERROR && (x) < temMALFORMED); + return (x >= telLOCAL_ERROR && x < temMALFORMED); } inline bool -isTemMalformed(TER x) +isTemMalformed(TER x) noexcept { - return ((x) >= temMALFORMED && (x) < tefFAILURE); + return (x >= temMALFORMED && x < tefFAILURE); } inline bool -isTefFailure(TER x) +isTefFailure(TER x) noexcept { - return ((x) >= tefFAILURE && (x) < terRETRY); + return (x >= tefFAILURE && x < terRETRY); } inline bool -isTerRetry(TER x) +isTerRetry(TER x) noexcept { - return ((x) >= terRETRY && (x) < tesSUCCESS); + return (x >= terRETRY && x < tesSUCCESS); } inline bool -isTesSuccess(TER x) +isTesSuccess(TER x) noexcept { - return ((x) == tesSUCCESS); + return (x == tesSUCCESS); } inline bool -isTecClaim(TER x) +isTecClaim(TER x) noexcept { return ((x) >= tecCLAIM); } diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index b2da99594a..71c8090b79 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -58,7 +58,8 @@ namespace ripple { // clang-format off // Universal Transaction flags: constexpr std::uint32_t tfFullyCanonicalSig = 0x80000000; -constexpr std::uint32_t tfUniversal = tfFullyCanonicalSig; +constexpr std::uint32_t tfInnerBatchTxn = 0x40000000; +constexpr std::uint32_t tfUniversal = tfFullyCanonicalSig | tfInnerBatchTxn; constexpr std::uint32_t tfUniversalMask = ~tfUniversal; // AccountSet flags: @@ -91,14 +92,16 @@ constexpr std::uint32_t asfDisallowIncomingCheck = 13; constexpr std::uint32_t asfDisallowIncomingPayChan = 14; constexpr std::uint32_t asfDisallowIncomingTrustline = 15; constexpr std::uint32_t asfAllowTrustLineClawback = 16; +constexpr std::uint32_t asfAllowTrustLineLocking = 17; // OfferCreate flags: constexpr std::uint32_t tfPassive = 0x00010000; constexpr std::uint32_t tfImmediateOrCancel = 0x00020000; constexpr std::uint32_t tfFillOrKill = 0x00040000; constexpr std::uint32_t tfSell = 0x00080000; +constexpr std::uint32_t tfHybrid = 0x00100000; constexpr std::uint32_t tfOfferCreateMask = - ~(tfUniversal | tfPassive | tfImmediateOrCancel | tfFillOrKill | tfSell); + ~(tfUniversal | tfPassive | tfImmediateOrCancel | tfFillOrKill | tfSell | tfHybrid); // Payment flags: constexpr std::uint32_t tfNoRippleDirect = 0x00010000; @@ -119,13 +122,7 @@ constexpr std::uint32_t tfClearDeepFreeze = 0x00800000; constexpr std::uint32_t tfTrustSetMask = ~(tfUniversal | tfSetfAuth | tfSetNoRipple | tfClearNoRipple | tfSetFreeze | tfClearFreeze | tfSetDeepFreeze | tfClearDeepFreeze); - -// valid flags for granular permission -constexpr std::uint32_t tfTrustSetGranularMask = tfSetfAuth | tfSetFreeze | tfClearFreeze; - -// bits representing supportedGranularMask are set to 0 and the bits -// representing other flags are set to 1 in tfPermissionMask. -constexpr std::uint32_t tfTrustSetPermissionMask = (~tfTrustSetMask) & (~tfTrustSetGranularMask); +constexpr std::uint32_t tfTrustSetPermissionMask = ~(tfUniversal | tfSetfAuth | tfSetFreeze | tfClearFreeze); // EnableAmendment flags: constexpr std::uint32_t tfGotMajority = 0x00010000; @@ -165,8 +162,7 @@ constexpr std::uint32_t const tfMPTokenAuthorizeMask = ~(tfUniversal | tfMPTUna constexpr std::uint32_t const tfMPTLock = 0x00000001; constexpr std::uint32_t const tfMPTUnlock = 0x00000002; constexpr std::uint32_t const tfMPTokenIssuanceSetMask = ~(tfUniversal | tfMPTLock | tfMPTUnlock); -constexpr std::uint32_t const tfMPTokenIssuanceSetGranularMask = tfMPTLock | tfMPTUnlock; -constexpr std::uint32_t const tfMPTokenIssuanceSetPermissionMask = (~tfMPTokenIssuanceSetMask) & (~tfMPTokenIssuanceSetGranularMask); +constexpr std::uint32_t const tfMPTokenIssuanceSetPermissionMask = ~(tfUniversal | tfMPTLock | tfMPTUnlock); // MPTokenIssuanceDestroy flags: constexpr std::uint32_t const tfMPTokenIssuanceDestroyMask = ~tfUniversal; @@ -242,6 +238,20 @@ constexpr std::uint32_t const tfVaultPrivate = 0x00010000; static_assert(tfVaultPrivate == lsfVaultPrivate); constexpr std::uint32_t const tfVaultShareNonTransferable = 0x00020000; constexpr std::uint32_t const tfVaultCreateMask = ~(tfUniversal | tfVaultPrivate | tfVaultShareNonTransferable); + +// Batch Flags: +constexpr std::uint32_t tfAllOrNothing = 0x00010000; +constexpr std::uint32_t tfOnlyOne = 0x00020000; +constexpr std::uint32_t tfUntilFailure = 0x00040000; +constexpr std::uint32_t tfIndependent = 0x00080000; +/** + * @note If nested Batch transactions are supported in the future, the tfInnerBatchTxn flag + * will need to be removed from this mask to allow Batch transaction to be inside + * the sfRawTransactions array. + */ +constexpr std::uint32_t const tfBatchMask = + ~(tfUniversal | tfAllOrNothing | tfOnlyOne | tfUntilFailure | tfIndependent) | tfInnerBatchTxn; + // clang-format on } // namespace ripple diff --git a/include/xrpl/protocol/TxMeta.h b/include/xrpl/protocol/TxMeta.h index 9422d697ca..02fde2ffe5 100644 --- a/include/xrpl/protocol/TxMeta.h +++ b/include/xrpl/protocol/TxMeta.h @@ -46,7 +46,10 @@ private: CtorHelper); public: - TxMeta(uint256 const& transactionID, std::uint32_t ledger); + TxMeta( + uint256 const& transactionID, + std::uint32_t ledger, + std::optional parentBatchId = std::nullopt); TxMeta(uint256 const& txID, std::uint32_t ledger, Blob const&); TxMeta(uint256 const& txID, std::uint32_t ledger, std::string const&); TxMeta(uint256 const& txID, std::uint32_t ledger, STObject const&); @@ -130,6 +133,27 @@ public: return static_cast(mDelivered); } + void + setParentBatchId(uint256 const& parentBatchId) + { + mParentBatchId = parentBatchId; + } + + uint256 + getParentBatchId() const + { + XRPL_ASSERT( + hasParentBatchId(), + "ripple::TxMeta::getParentBatchId : non-null batch id"); + return *mParentBatchId; + } + + bool + hasParentBatchId() const + { + return static_cast(mParentBatchId); + } + private: uint256 mTransactionID; std::uint32_t mLedger; @@ -137,6 +161,7 @@ private: int mResult; std::optional mDelivered; + std::optional mParentBatchId; STArray mNodes; }; diff --git a/include/xrpl/protocol/UintTypes.h b/include/xrpl/protocol/UintTypes.h index 9a7284158e..1d6b3e23dc 100644 --- a/include/xrpl/protocol/UintTypes.h +++ b/include/xrpl/protocol/UintTypes.h @@ -63,6 +63,9 @@ using NodeID = base_uint<160, detail::NodeIDTag>; * and a 160-bit account */ using MPTID = base_uint<192>; +/** Domain is a 256-bit hash representing a specific domain. */ +using Domain = base_uint<256>; + /** XRP currency. */ Currency const& xrpCurrency(); @@ -119,25 +122,25 @@ namespace std { template <> struct hash : ripple::Currency::hasher { - explicit hash() = default; + hash() = default; }; template <> struct hash : ripple::NodeID::hasher { - explicit hash() = default; + hash() = default; }; template <> struct hash : ripple::Directory::hasher { - explicit hash() = default; + hash() = default; }; template <> struct hash : ripple::uint256::hasher { - explicit hash() = default; + hash() = default; }; } // namespace std diff --git a/include/xrpl/protocol/Units.h b/include/xrpl/protocol/Units.h index f92623eec6..d6085891e6 100644 --- a/include/xrpl/protocol/Units.h +++ b/include/xrpl/protocol/Units.h @@ -27,14 +27,9 @@ #include #include -#include -#include #include #include #include -#include -#include -#include namespace ripple { diff --git a/include/xrpl/protocol/XChainAttestations.h b/include/xrpl/protocol/XChainAttestations.h index 721950ca9c..92fd04731d 100644 --- a/include/xrpl/protocol/XChainAttestations.h +++ b/include/xrpl/protocol/XChainAttestations.h @@ -35,8 +35,6 @@ #include #include -#include -#include #include namespace ripple { diff --git a/include/xrpl/protocol/detail/b58_utils.h b/include/xrpl/protocol/detail/b58_utils.h index 8fc85f390b..ecd301524f 100644 --- a/include/xrpl/protocol/detail/b58_utils.h +++ b/include/xrpl/protocol/detail/b58_utils.h @@ -27,7 +27,6 @@ #include #include -#include #include #include #include diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 3be0fd426c..63c1b2258b 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -26,12 +26,21 @@ #if !defined(XRPL_RETIRE) #error "undefined macro: XRPL_RETIRE" #endif +#if !defined(XRPL_ABANDON) +#error "undefined macro: XRPL_ABANDON" +#endif // Add new amendments to the top of this list. // Keep it sorted in reverse chronological order. // If you add an amendment here, then do not forget to increment `numFeatures` // in include/xrpl/protocol/Feature.h. +XRPL_FIX (AMMClawbackRounding, Supported::no, VoteBehavior::DefaultNo) +XRPL_FEATURE(TokenEscrow, Supported::yes, VoteBehavior::DefaultNo) +XRPL_FIX (EnforceNFTokenTrustlineV2, Supported::yes, VoteBehavior::DefaultNo) +XRPL_FIX (AMMv1_3, Supported::yes, VoteBehavior::DefaultNo) +XRPL_FEATURE(PermissionedDEX, Supported::yes, VoteBehavior::DefaultNo) +XRPL_FEATURE(Batch, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(SingleAssetVault, Supported::no, VoteBehavior::DefaultNo) XRPL_FEATURE(PermissionDelegation, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (PayChanCancelAfter, Supported::yes, VoteBehavior::DefaultNo) @@ -89,7 +98,6 @@ XRPL_FEATURE(HardenedValidations, Supported::yes, VoteBehavior::DefaultYe // fix1781: XRPEndpointSteps should be included in the circular payment check XRPL_FIX (1781, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(RequireFullyCanonicalSig, Supported::yes, VoteBehavior::DefaultYes) -// fixQualityUpperBound should be activated before FlowCross XRPL_FIX (QualityUpperBound, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(DeletableAccounts, Supported::yes, VoteBehavior::DefaultYes) XRPL_FIX (PayChanRecipientOwnerDir, Supported::yes, VoteBehavior::DefaultYes) @@ -107,9 +115,7 @@ XRPL_FIX (1571, Supported::yes, VoteBehavior::DefaultYe XRPL_FEATURE(Checks, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(DepositAuth, Supported::yes, VoteBehavior::DefaultYes) XRPL_FIX (1513, Supported::yes, VoteBehavior::DefaultYes) -XRPL_FEATURE(FlowCross, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(Flow, Supported::yes, VoteBehavior::DefaultYes) -XRPL_FEATURE(OwnerPaysFee, Supported::no, VoteBehavior::DefaultNo) // The following amendments are obsolete, but must remain supported // because they could potentially get enabled. @@ -127,6 +133,11 @@ XRPL_FIX (NFTokenDirV1, Supported::yes, VoteBehavior::Obsolete) XRPL_FEATURE(NonFungibleTokensV1, Supported::yes, VoteBehavior::Obsolete) XRPL_FEATURE(CryptoConditionsSuite, Supported::yes, VoteBehavior::Obsolete) +// The following amendments were never supported, never enabled, and +// we've abanded them. These features should never be in the ledger, +// and we've removed all the related code. +XRPL_ABANDON(OwnerPaysFee) + // The following amendments have been active for at least two years. Their // pre-amendment code has been removed and the identifiers are deprecated. // All known amendments and amendments that may appear in a validated @@ -146,3 +157,4 @@ XRPL_RETIRE(fix1201) XRPL_RETIRE(fix1512) XRPL_RETIRE(fix1523) XRPL_RETIRE(fix1528) +XRPL_RETIRE(FlowCross) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index a902b32026..46c6e60db3 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -188,6 +188,7 @@ LEDGER_ENTRY(ltDIR_NODE, 0x0064, DirectoryNode, directory, ({ {sfNFTokenID, soeOPTIONAL}, {sfPreviousTxnID, soeOPTIONAL}, {sfPreviousTxnLgrSeq, soeOPTIONAL}, + {sfDomainID, soeOPTIONAL} })) /** The ledger object which lists details about amendments on the network. @@ -249,6 +250,8 @@ LEDGER_ENTRY(ltOFFER, 0x006f, Offer, offer, ({ {sfPreviousTxnID, soeREQUIRED}, {sfPreviousTxnLgrSeq, soeREQUIRED}, {sfExpiration, soeOPTIONAL}, + {sfDomainID, soeOPTIONAL}, + {sfAdditionalBooks, soeOPTIONAL}, })) /** A ledger object which describes a deposit preauthorization. @@ -351,6 +354,8 @@ LEDGER_ENTRY(ltESCROW, 0x0075, Escrow, escrow, ({ {sfPreviousTxnID, soeREQUIRED}, {sfPreviousTxnLgrSeq, soeREQUIRED}, {sfDestinationNode, soeOPTIONAL}, + {sfTransferRate, soeOPTIONAL}, + {sfIssuerNode, soeOPTIONAL}, })) /** A ledger object describing a single unidirectional XRP payment channel. @@ -402,6 +407,7 @@ LEDGER_ENTRY(ltMPTOKEN_ISSUANCE, 0x007e, MPTokenIssuance, mpt_issuance, ({ {sfAssetScale, soeDEFAULT}, {sfMaximumAmount, soeOPTIONAL}, {sfOutstandingAmount, soeREQUIRED}, + {sfLockedAmount, soeOPTIONAL}, {sfMPTokenMetadata, soeOPTIONAL}, {sfPreviousTxnID, soeREQUIRED}, {sfPreviousTxnLgrSeq, soeREQUIRED}, @@ -415,6 +421,7 @@ LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, mptoken, ({ {sfAccount, soeREQUIRED}, {sfMPTokenIssuanceID, soeREQUIRED}, {sfMPTAmount, soeDEFAULT}, + {sfLockedAmount, soeOPTIONAL}, {sfOwnerNode, soeREQUIRED}, {sfPreviousTxnID, soeREQUIRED}, {sfPreviousTxnLgrSeq, soeREQUIRED}, diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 40d118c684..17713bd2ad 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -144,6 +144,7 @@ TYPED_SFIELD(sfOutstandingAmount, UINT64, 25, SField::sMD_BaseTen|SFie TYPED_SFIELD(sfMPTAmount, UINT64, 26, SField::sMD_BaseTen|SField::sMD_Default) TYPED_SFIELD(sfIssuerNode, UINT64, 27) TYPED_SFIELD(sfSubjectNode, UINT64, 28) +TYPED_SFIELD(sfLockedAmount, UINT64, 29, SField::sMD_BaseTen|SField::sMD_Default) // 128-bit TYPED_SFIELD(sfEmailHash, UINT128, 1) @@ -197,6 +198,7 @@ TYPED_SFIELD(sfHookSetTxnID, UINT256, 33) TYPED_SFIELD(sfDomainID, UINT256, 34) TYPED_SFIELD(sfVaultID, UINT256, 35, SField::sMD_PseudoAccount | SField::sMD_Default) +TYPED_SFIELD(sfParentBatchID, UINT256, 36) // number (common) TYPED_SFIELD(sfNumber, NUMBER, 1) @@ -359,6 +361,9 @@ UNTYPED_SFIELD(sfXChainClaimAttestationCollectionElement, OBJECT, 30) UNTYPED_SFIELD(sfXChainCreateAccountAttestationCollectionElement, OBJECT, 31) UNTYPED_SFIELD(sfPriceData, OBJECT, 32) UNTYPED_SFIELD(sfCredential, OBJECT, 33) +UNTYPED_SFIELD(sfRawTransaction, OBJECT, 34) +UNTYPED_SFIELD(sfBatchSigner, OBJECT, 35) +UNTYPED_SFIELD(sfBook, OBJECT, 36) // array of objects (common) // ARRAY/1 is reserved for end of array @@ -374,6 +379,7 @@ UNTYPED_SFIELD(sfMemos, ARRAY, 9) UNTYPED_SFIELD(sfNFTokens, ARRAY, 10) UNTYPED_SFIELD(sfHooks, ARRAY, 11) UNTYPED_SFIELD(sfVoteSlots, ARRAY, 12) +UNTYPED_SFIELD(sfAdditionalBooks, ARRAY, 13) // array of objects (uncommon) UNTYPED_SFIELD(sfMajorities, ARRAY, 16) @@ -390,3 +396,5 @@ UNTYPED_SFIELD(sfAuthorizeCredentials, ARRAY, 26) UNTYPED_SFIELD(sfUnauthorizeCredentials, ARRAY, 27) UNTYPED_SFIELD(sfAcceptedCredentials, ARRAY, 28) UNTYPED_SFIELD(sfPermissions, ARRAY, 29) +UNTYPED_SFIELD(sfRawTransactions, ARRAY, 30) +UNTYPED_SFIELD(sfBatchSigners, ARRAY, 31, SField::sMD_Default, SField::notSigning) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index b61bf3135f..521044241a 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -71,6 +71,7 @@ TRANSACTION(ttPAYMENT, 0, Payment, {sfDestinationTag, soeOPTIONAL}, {sfDeliverMin, soeOPTIONAL, soeMPTSupported}, {sfCredentialIDs, soeOPTIONAL}, + {sfDomainID, soeOPTIONAL}, })) /** This transaction type creates an escrow object. */ @@ -81,7 +82,7 @@ TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate, Delegation::delegatable, noPriv, ({ {sfDestination, soeREQUIRED}, - {sfAmount, soeREQUIRED}, + {sfAmount, soeREQUIRED, soeMPTSupported}, {sfCondition, soeOPTIONAL}, {sfCancelAfter, soeOPTIONAL}, {sfFinishAfter, soeOPTIONAL}, @@ -153,6 +154,7 @@ TRANSACTION(ttOFFER_CREATE, 7, OfferCreate, {sfTakerGets, soeREQUIRED}, {sfExpiration, soeOPTIONAL}, {sfOfferSequence, soeOPTIONAL}, + {sfDomainID, soeOPTIONAL}, })) /** This transaction type cancels existing offers to trade one asset for another. */ @@ -814,6 +816,18 @@ TRANSACTION(ttVAULT_CLAWBACK, 70, VaultClawback, {sfAmount, soeOPTIONAL, soeMPTSupported}, })) +/** This transaction type batches together transactions. */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttBATCH, 71, Batch, + Delegation::notDelegatable, + noPriv, + ({ + {sfRawTransactions, soeREQUIRED}, + {sfBatchSigners, soeOPTIONAL}, +})) + /** This system-generated transaction type is used to update the status of the various amendments. For details, see: https://xrpl.org/amendments.html @@ -857,4 +871,3 @@ TRANSACTION(ttUNL_MODIFY, 102, UNLModify, {sfLedgerSequence, soeREQUIRED}, {sfUNLModifyValidator, soeREQUIRED}, })) - diff --git a/include/xrpl/protocol/digest.h b/include/xrpl/protocol/digest.h index efec616a0c..303fbafe4f 100644 --- a/include/xrpl/protocol/digest.h +++ b/include/xrpl/protocol/digest.h @@ -25,7 +25,6 @@ #include -#include #include namespace ripple { diff --git a/include/xrpl/protocol/json_get_or_throw.h b/include/xrpl/protocol/json_get_or_throw.h index c59b5a71a3..74d1779339 100644 --- a/include/xrpl/protocol/json_get_or_throw.h +++ b/include/xrpl/protocol/json_get_or_throw.h @@ -10,7 +10,6 @@ #include #include #include -#include namespace Json { struct JsonMissingKeyError : std::exception diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index fc7f367562..2adf06d075 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -83,6 +83,8 @@ JSS(PriceDataSeries); // field. JSS(PriceData); // field. JSS(Provider); // field. JSS(QuoteAsset); // in: Oracle. +JSS(RawTransaction); // in: Batch +JSS(RawTransactions); // in: Batch JSS(SLE_hit_rate); // out: GetCounts. JSS(Scale); // field. JSS(SettleDelay); // in: TransactionSign @@ -389,6 +391,7 @@ JSS(load_fee); // out: LoadFeeTrackImp, NetworkOPs JSS(local); // out: resource/Logic.h JSS(local_txs); // out: GetCounts JSS(local_static_keys); // out: ValidatorList +JSS(locked); // out: GatewayBalances JSS(low); // out: BookChanges JSS(lowest_sequence); // out: AccountInfo JSS(lowest_ticket); // out: AccountInfo diff --git a/include/xrpl/resource/Charge.h b/include/xrpl/resource/Charge.h index a75ad32624..ead46ca31f 100644 --- a/include/xrpl/resource/Charge.h +++ b/include/xrpl/resource/Charge.h @@ -20,7 +20,6 @@ #ifndef RIPPLE_RESOURCE_CHARGE_H_INCLUDED #define RIPPLE_RESOURCE_CHARGE_H_INCLUDED -#include #include namespace ripple { diff --git a/include/xrpl/resource/Gossip.h b/include/xrpl/resource/Gossip.h index 6e2a86ecd7..3495de5b95 100644 --- a/include/xrpl/resource/Gossip.h +++ b/include/xrpl/resource/Gossip.h @@ -22,6 +22,8 @@ #include +#include + namespace ripple { namespace Resource { diff --git a/include/xrpl/resource/detail/Key.h b/include/xrpl/resource/detail/Key.h index f953d5103e..188ee142da 100644 --- a/include/xrpl/resource/detail/Key.h +++ b/include/xrpl/resource/detail/Key.h @@ -53,7 +53,7 @@ struct Key struct key_equal { - explicit key_equal() = default; + key_equal() = default; bool operator()(Key const& lhs, Key const& rhs) const diff --git a/include/xrpl/server/detail/BaseHTTPPeer.h b/include/xrpl/server/detail/BaseHTTPPeer.h index 51ac866e1e..b065a97cf0 100644 --- a/include/xrpl/server/detail/BaseHTTPPeer.h +++ b/include/xrpl/server/detail/BaseHTTPPeer.h @@ -41,7 +41,6 @@ #include #include #include -#include #include namespace ripple { diff --git a/include/xrpl/server/detail/Door.h b/include/xrpl/server/detail/Door.h index 90de885579..88e19db8cd 100644 --- a/include/xrpl/server/detail/Door.h +++ b/include/xrpl/server/detail/Door.h @@ -37,10 +37,8 @@ #include #include -#include #include #include -#include namespace ripple { diff --git a/include/xrpl/server/detail/io_list.h b/include/xrpl/server/detail/io_list.h index fbf60c9a7f..fba8b28f87 100644 --- a/include/xrpl/server/detail/io_list.h +++ b/include/xrpl/server/detail/io_list.h @@ -26,7 +26,6 @@ #include #include #include -#include #include #include diff --git a/src/libxrpl/protocol/Book.cpp b/src/libxrpl/protocol/Book.cpp index cfd1fc61dc..2114deab6b 100644 --- a/src/libxrpl/protocol/Book.cpp +++ b/src/libxrpl/protocol/Book.cpp @@ -48,7 +48,7 @@ operator<<(std::ostream& os, Book const& x) Book reversed(Book const& book) { - return Book(book.out, book.in); + return Book(book.out, book.in, book.domain); } } // namespace ripple diff --git a/src/libxrpl/protocol/BuildInfo.cpp b/src/libxrpl/protocol/BuildInfo.cpp index 1f061cebdc..4cb6fbfd36 100644 --- a/src/libxrpl/protocol/BuildInfo.cpp +++ b/src/libxrpl/protocol/BuildInfo.cpp @@ -36,7 +36,7 @@ namespace BuildInfo { // and follow the format described at http://semver.org/ //------------------------------------------------------------------------------ // clang-format off -char const* const versionString = "2.5.0-b1" +char const* const versionString = "2.5.0" // clang-format on #if defined(DEBUG) || defined(SANITIZER) diff --git a/src/libxrpl/protocol/ErrorCodes.cpp b/src/libxrpl/protocol/ErrorCodes.cpp index b3d1b812b5..3109f51d05 100644 --- a/src/libxrpl/protocol/ErrorCodes.cpp +++ b/src/libxrpl/protocol/ErrorCodes.cpp @@ -116,7 +116,8 @@ constexpr static ErrorInfo unorderedErrorInfos[]{ {rpcUNKNOWN_COMMAND, "unknownCmd", "Unknown method.", 405}, {rpcORACLE_MALFORMED, "oracleMalformed", "Oracle request is malformed.", 400}, {rpcBAD_CREDENTIALS, "badCredentials", "Credentials do not exist, are not accepted, or have expired.", 400}, - {rpcTX_SIGNED, "transactionSigned", "Transaction should not be signed.", 400}}; + {rpcTX_SIGNED, "transactionSigned", "Transaction should not be signed.", 400}, + {rpcDOMAIN_MALFORMED, "domainMalformed", "Domain is malformed.", 400}}; // clang-format on // Sort and validate unorderedErrorInfos at compile time. Should be diff --git a/src/libxrpl/protocol/Feature.cpp b/src/libxrpl/protocol/Feature.cpp index eeeee1c185..478b155387 100644 --- a/src/libxrpl/protocol/Feature.cpp +++ b/src/libxrpl/protocol/Feature.cpp @@ -254,7 +254,7 @@ FeatureCollections::registerFeature( { check(!readOnly, "Attempting to register a feature after startup."); check( - support == Supported::yes || vote == VoteBehavior::DefaultNo, + support == Supported::yes || vote != VoteBehavior::DefaultYes, "Invalid feature parameters. Must be supported to be up-voted."); Feature const* i = getByName(name); if (!i) @@ -268,7 +268,7 @@ FeatureCollections::registerFeature( features.emplace_back(name, f); auto const getAmendmentSupport = [=]() { - if (vote == VoteBehavior::Obsolete) + if (vote == VoteBehavior::Obsolete && support == Supported::yes) return AmendmentSupport::Retired; return support == Supported::yes ? AmendmentSupport::Supported : AmendmentSupport::Unsupported; @@ -398,6 +398,14 @@ retireFeature(std::string const& name) return registerFeature(name, Supported::yes, VoteBehavior::Obsolete); } +// Abandoned features are not in the ledger and have no code controlled by the +// feature. They were never supported, and cannot be voted on. +uint256 +abandonFeature(std::string const& name) +{ + return registerFeature(name, Supported::no, VoteBehavior::Obsolete); +} + /** Tell FeatureCollections when registration is complete. */ bool registrationIsDone() @@ -432,6 +440,8 @@ featureToName(uint256 const& f) #undef XRPL_FIX #pragma push_macro("XRPL_RETIRE") #undef XRPL_RETIRE +#pragma push_macro("XRPL_ABANDON") +#undef XRPL_ABANDON #define XRPL_FEATURE(name, supported, vote) \ uint256 const feature##name = registerFeature(#name, supported, vote); @@ -443,6 +453,11 @@ featureToName(uint256 const& f) [[deprecated("The referenced amendment has been retired")]] \ [[maybe_unused]] \ uint256 const retired##name = retireFeature(#name); + +#define XRPL_ABANDON(name) \ + [[deprecated("The referenced amendment has been abandoned")]] \ + [[maybe_unused]] \ + uint256 const abandoned##name = abandonFeature(#name); // clang-format on #include @@ -453,6 +468,8 @@ featureToName(uint256 const& f) #pragma pop_macro("XRPL_FIX") #undef XRPL_FEATURE #pragma pop_macro("XRPL_FEATURE") +#undef XRPL_ABANDON +#pragma pop_macro("XRPL_ABANDON") // All of the features should now be registered, since variables in a cpp file // are initialized from top to bottom. diff --git a/src/libxrpl/protocol/Indexes.cpp b/src/libxrpl/protocol/Indexes.cpp index 2426092d13..486945992a 100644 --- a/src/libxrpl/protocol/Indexes.cpp +++ b/src/libxrpl/protocol/Indexes.cpp @@ -117,12 +117,19 @@ getBookBase(Book const& book) XRPL_ASSERT( isConsistent(book), "ripple::getBookBase : input is consistent"); - auto const index = indexHash( - LedgerNameSpace::BOOK_DIR, - book.in.currency, - book.out.currency, - book.in.account, - book.out.account); + auto const index = book.domain ? indexHash( + LedgerNameSpace::BOOK_DIR, + book.in.currency, + book.out.currency, + book.in.account, + book.out.account, + *(book.domain)) + : indexHash( + LedgerNameSpace::BOOK_DIR, + book.in.currency, + book.out.currency, + book.in.account, + book.out.account); // Return with quality 0. auto k = keylet::quality({ltDIR_NODE, index}, 0); diff --git a/src/libxrpl/protocol/InnerObjectFormats.cpp b/src/libxrpl/protocol/InnerObjectFormats.cpp index ecfca9743d..2de5e6624e 100644 --- a/src/libxrpl/protocol/InnerObjectFormats.cpp +++ b/src/libxrpl/protocol/InnerObjectFormats.cpp @@ -158,6 +158,20 @@ InnerObjectFormats::InnerObjectFormats() add(sfPermission.jsonName.c_str(), sfPermission.getCode(), {{sfPermissionValue, soeREQUIRED}}); + + add(sfBatchSigner.jsonName.c_str(), + sfBatchSigner.getCode(), + {{sfAccount, soeREQUIRED}, + {sfSigningPubKey, soeOPTIONAL}, + {sfTxnSignature, soeOPTIONAL}, + {sfSigners, soeOPTIONAL}}); + + add(sfBook.jsonName, + sfBook.getCode(), + { + {sfBookDirectory, soeREQUIRED}, + {sfBookNode, soeREQUIRED}, + }); } InnerObjectFormats const& diff --git a/src/libxrpl/protocol/NFTSyntheticSerializer.cpp b/src/libxrpl/protocol/NFTSyntheticSerializer.cpp index 0c0a657512..64fa9319de 100644 --- a/src/libxrpl/protocol/NFTSyntheticSerializer.cpp +++ b/src/libxrpl/protocol/NFTSyntheticSerializer.cpp @@ -28,6 +28,7 @@ #include namespace ripple { +namespace RPC { void insertNFTSyntheticInJson( @@ -39,4 +40,5 @@ insertNFTSyntheticInJson( insertNFTokenOfferID(response[jss::meta], transaction, transactionMeta); } +} // namespace RPC } // namespace ripple diff --git a/src/libxrpl/protocol/Rules.cpp b/src/libxrpl/protocol/Rules.cpp index 3d1c718e65..b472b9b0f1 100644 --- a/src/libxrpl/protocol/Rules.cpp +++ b/src/libxrpl/protocol/Rules.cpp @@ -161,4 +161,12 @@ Rules::operator!=(Rules const& other) const { return !(*this == other); } + +bool +isFeatureEnabled(uint256 const& feature) +{ + auto const& rules = getCurrentTransactionRules(); + return rules && rules->enabled(feature); +} + } // namespace ripple diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 02de5d4c58..0c72244885 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -506,6 +506,162 @@ getRate(STAmount const& offerOut, STAmount const& offerIn) return 0; } +/** + * @brief Safely checks if two STAmount values can be added without overflow, + * underflow, or precision loss. + * + * This function determines whether the addition of two STAmount objects is + * safe, depending on their type: + * - For XRP amounts, it checks for integer overflow and underflow. + * - For IOU amounts, it checks for acceptable precision loss. + * - For MPT amounts, it checks for overflow and underflow within 63-bit signed + * integer limits. + * - If either amount is zero, addition is always considered safe. + * - If the amounts are of different currencies or types, addition is not + * allowed. + * + * @param a The first STAmount to add. + * @param b The second STAmount to add. + * @return true if the addition is safe; false otherwise. + */ +bool +canAdd(STAmount const& a, STAmount const& b) +{ + // cannot add different currencies + if (!areComparable(a, b)) + return false; + + // special case: adding anything to zero is always fine + if (a == beast::zero || b == beast::zero) + return true; + + // XRP case (overflow & underflow check) + if (isXRP(a) && isXRP(b)) + { + XRPAmount A = a.xrp(); + XRPAmount B = b.xrp(); + + if ((B > XRPAmount{0} && + A > XRPAmount{std::numeric_limits::max()} - + B) || + (B < XRPAmount{0} && + A < XRPAmount{std::numeric_limits::min()} - + B)) + { + return false; + } + return true; + } + + // IOU case (precision check) + if (a.holds() && b.holds()) + { + static STAmount const one{IOUAmount{1, 0}, noIssue()}; + static STAmount const maxLoss{IOUAmount{1, -4}, noIssue()}; + STAmount lhs = divide((a - b) + b, a, noIssue()) - one; + STAmount rhs = divide((b - a) + a, b, noIssue()) - one; + return ((rhs.negative() ? -rhs : rhs) + + (lhs.negative() ? -lhs : lhs)) <= maxLoss; + } + + // MPT (overflow & underflow check) + if (a.holds() && b.holds()) + { + MPTAmount A = a.mpt(); + MPTAmount B = b.mpt(); + if ((B > MPTAmount{0} && + A > MPTAmount{std::numeric_limits::max()} - + B) || + (B < MPTAmount{0} && + A < MPTAmount{std::numeric_limits::min()} - + B)) + { + return false; + } + + return true; + } + // LCOV_EXCL_START + UNREACHABLE("STAmount::canAdd : unexpected STAmount type"); + return false; + // LCOV_EXCL_STOP +} + +/** + * @brief Determines if it is safe to subtract one STAmount from another. + * + * This function checks whether subtracting amount `b` from amount `a` is valid, + * considering currency compatibility and underflow conditions for specific + * types. + * + * - Subtracting zero is always allowed. + * - Subtraction is only allowed between comparable currencies. + * - For XRP amounts, ensures no underflow or overflow occurs. + * - For IOU amounts, subtraction is always allowed (no underflow). + * - For MPT amounts, ensures no underflow or overflow occurs. + * + * @param a The minuend (amount to subtract from). + * @param b The subtrahend (amount to subtract). + * @return true if subtraction is allowed, false otherwise. + */ +bool +canSubtract(STAmount const& a, STAmount const& b) +{ + // Cannot subtract different currencies + if (!areComparable(a, b)) + return false; + + // Special case: subtracting zero is always fine + if (b == beast::zero) + return true; + + // XRP case (underflow & overflow check) + if (isXRP(a) && isXRP(b)) + { + XRPAmount A = a.xrp(); + XRPAmount B = b.xrp(); + // Check for underflow + if (B > XRPAmount{0} && A < B) + return false; + + // Check for overflow + if (B < XRPAmount{0} && + A > XRPAmount{std::numeric_limits::max()} + + B) + return false; + + return true; + } + + // IOU case (no underflow) + if (a.holds() && b.holds()) + { + return true; + } + + // MPT case (underflow & overflow check) + if (a.holds() && b.holds()) + { + MPTAmount A = a.mpt(); + MPTAmount B = b.mpt(); + + // Underflow check + if (B > MPTAmount{0} && A < B) + return false; + + // Overflow check + if (B < MPTAmount{0} && + A > MPTAmount{std::numeric_limits::max()} + + B) + return false; + return true; + } + // LCOV_EXCL_START + UNREACHABLE("STAmount::canSubtract : unexpected STAmount type"); + return false; + // LCOV_EXCL_STOP +} + void STAmount::setJson(Json::Value& elem) const { diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index 7b6b4c1ee2..615012dba4 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -29,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -42,6 +44,7 @@ #include #include #include +#include #include #include #include @@ -262,6 +265,42 @@ STTx::checkSign( return Unexpected("Internal signature check failure."); } +Expected +STTx::checkBatchSign( + RequireFullyCanonicalSig requireCanonicalSig, + Rules const& rules) const +{ + try + { + XRPL_ASSERT( + getTxnType() == ttBATCH, + "STTx::checkBatchSign : not a batch transaction"); + if (getTxnType() != ttBATCH) + { + JLOG(debugLog().fatal()) << "not a batch transaction"; + return Unexpected("Not a batch transaction."); + } + STArray const& signers{getFieldArray(sfBatchSigners)}; + for (auto const& signer : signers) + { + Blob const& signingPubKey = signer.getFieldVL(sfSigningPubKey); + auto const result = signingPubKey.empty() + ? checkBatchMultiSign(signer, requireCanonicalSig, rules) + : checkBatchSingleSign(signer, requireCanonicalSig); + + if (!result) + return result; + } + return {}; + } + catch (std::exception const& e) + { + JLOG(debugLog().error()) + << "Batch signature check failed: " << e.what(); + } + return Unexpected("Internal batch signature check failure."); +} + Json::Value STTx::getJson(JsonOptions options) const { @@ -341,79 +380,90 @@ STTx::getMetaSQL( getFieldU32(sfSequence) % inLedger % status % rTxn % escapedMetaData); } -Expected -STTx::checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const +static Expected +singleSignHelper( + STObject const& signer, + Slice const& data, + bool const fullyCanonical) { // We don't allow both a non-empty sfSigningPubKey and an sfSigners. // That would allow the transaction to be signed two ways. So if both // fields are present the signature is invalid. - if (isFieldPresent(sfSigners)) + if (signer.isFieldPresent(sfSigners)) return Unexpected("Cannot both single- and multi-sign."); bool validSig = false; try { - bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) || - (requireCanonicalSig == RequireFullyCanonicalSig::yes); - - auto const spk = getFieldVL(sfSigningPubKey); - + auto const spk = signer.getFieldVL(sfSigningPubKey); if (publicKeyType(makeSlice(spk))) { - Blob const signature = getFieldVL(sfTxnSignature); - Blob const data = getSigningData(*this); - + Blob const signature = signer.getFieldVL(sfTxnSignature); validSig = verify( PublicKey(makeSlice(spk)), - makeSlice(data), + data, makeSlice(signature), fullyCanonical); } } catch (std::exception const&) { - // Assume it was a signature failure. validSig = false; } - if (validSig == false) + + if (!validSig) return Unexpected("Invalid signature."); - // Signature was verified. + return {}; } Expected -STTx::checkMultiSign( - RequireFullyCanonicalSig requireCanonicalSig, - Rules const& rules) const +STTx::checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const +{ + auto const data = getSigningData(*this); + bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) || + (requireCanonicalSig == STTx::RequireFullyCanonicalSig::yes); + return singleSignHelper(*this, makeSlice(data), fullyCanonical); +} + +Expected +STTx::checkBatchSingleSign( + STObject const& batchSigner, + RequireFullyCanonicalSig requireCanonicalSig) const +{ + Serializer msg; + serializeBatch(msg, getFlags(), getBatchTransactionIDs()); + bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) || + (requireCanonicalSig == STTx::RequireFullyCanonicalSig::yes); + return singleSignHelper(batchSigner, msg.slice(), fullyCanonical); +} + +Expected +multiSignHelper( + STObject const& signerObj, + bool const fullyCanonical, + std::function makeMsg, + Rules const& rules) { // Make sure the MultiSigners are present. Otherwise they are not // attempting multi-signing and we just have a bad SigningPubKey. - if (!isFieldPresent(sfSigners)) + if (!signerObj.isFieldPresent(sfSigners)) return Unexpected("Empty SigningPubKey."); // We don't allow both an sfSigners and an sfTxnSignature. Both fields // being present would indicate that the transaction is signed both ways. - if (isFieldPresent(sfTxnSignature)) + if (signerObj.isFieldPresent(sfTxnSignature)) return Unexpected("Cannot both single- and multi-sign."); - STArray const& signers{getFieldArray(sfSigners)}; + STArray const& signers{signerObj.getFieldArray(sfSigners)}; // There are well known bounds that the number of signers must be within. - if (signers.size() < minMultiSigners || - signers.size() > maxMultiSigners(&rules)) + if (signers.size() < STTx::minMultiSigners || + signers.size() > STTx::maxMultiSigners(&rules)) return Unexpected("Invalid Signers array size."); - // We can ease the computational load inside the loop a bit by - // pre-constructing part of the data that we hash. Fill a Serializer - // with the stuff that stays constant from signature to signature. - Serializer const dataStart{startMultiSigningData(*this)}; - // We also use the sfAccount field inside the loop. Get it once. - auto const txnAccountID = getAccountID(sfAccount); - - // Determine whether signatures must be full canonical. - bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) || - (requireCanonicalSig == RequireFullyCanonicalSig::yes); + auto const txnAccountID = signerObj.getAccountID(sfAccount); // Signers must be in sorted order by AccountID. AccountID lastAccountID(beast::zero); @@ -441,18 +491,13 @@ STTx::checkMultiSign( bool validSig = false; try { - Serializer s = dataStart; - finishMultiSigningData(accountID, s); - auto spk = signer.getFieldVL(sfSigningPubKey); - if (publicKeyType(makeSlice(spk))) { Blob const signature = signer.getFieldVL(sfTxnSignature); - validSig = verify( PublicKey(makeSlice(spk)), - s.slice(), + makeMsg(accountID).slice(), makeSlice(signature), fullyCanonical); } @@ -471,6 +516,90 @@ STTx::checkMultiSign( return {}; } +Expected +STTx::checkBatchMultiSign( + STObject const& batchSigner, + RequireFullyCanonicalSig requireCanonicalSig, + Rules const& rules) const +{ + bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) || + (requireCanonicalSig == RequireFullyCanonicalSig::yes); + + // We can ease the computational load inside the loop a bit by + // pre-constructing part of the data that we hash. Fill a Serializer + // with the stuff that stays constant from signature to signature. + Serializer dataStart; + serializeBatch(dataStart, getFlags(), getBatchTransactionIDs()); + return multiSignHelper( + batchSigner, + fullyCanonical, + [&dataStart](AccountID const& accountID) mutable -> Serializer { + Serializer s = dataStart; + finishMultiSigningData(accountID, s); + return s; + }, + rules); +} + +Expected +STTx::checkMultiSign( + RequireFullyCanonicalSig requireCanonicalSig, + Rules const& rules) const +{ + bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) || + (requireCanonicalSig == RequireFullyCanonicalSig::yes); + + // We can ease the computational load inside the loop a bit by + // pre-constructing part of the data that we hash. Fill a Serializer + // with the stuff that stays constant from signature to signature. + Serializer dataStart = startMultiSigningData(*this); + return multiSignHelper( + *this, + fullyCanonical, + [&dataStart](AccountID const& accountID) mutable -> Serializer { + Serializer s = dataStart; + finishMultiSigningData(accountID, s); + return s; + }, + rules); +} + +/** + * @brief Retrieves a batch of transaction IDs from the STTx. + * + * This function returns a vector of transaction IDs by extracting them from + * the field array `sfRawTransactions` within the STTx. If the batch + * transaction IDs have already been computed and cached in `batch_txn_ids_`, + * it returns the cached vector. Otherwise, it computes the transaction IDs, + * caches them, and then returns the vector. + * + * @return A vector of `uint256` containing the batch transaction IDs. + * + * @note The function asserts that the `sfRawTransactions` field array is not + * empty and that the size of the computed batch transaction IDs matches the + * size of the `sfRawTransactions` field array. + */ +std::vector +STTx::getBatchTransactionIDs() const +{ + XRPL_ASSERT( + getTxnType() == ttBATCH, + "STTx::getBatchTransactionIDs : not a batch transaction"); + XRPL_ASSERT( + getFieldArray(sfRawTransactions).size() != 0, + "STTx::getBatchTransactionIDs : empty raw transactions"); + if (batch_txn_ids_.size() != 0) + return batch_txn_ids_; + + for (STObject const& rb : getFieldArray(sfRawTransactions)) + batch_txn_ids_.push_back(rb.getHash(HashPrefix::transactionID)); + + XRPL_ASSERT( + batch_txn_ids_.size() == getFieldArray(sfRawTransactions).size(), + "STTx::getBatchTransactionIDs : batch transaction IDs size mismatch"); + return batch_txn_ids_; +} + //------------------------------------------------------------------------------ static bool @@ -606,6 +735,48 @@ invalidMPTAmountInTx(STObject const& tx) return false; } +static bool +isRawTransactionOkay(STObject const& st, std::string& reason) +{ + if (!st.isFieldPresent(sfRawTransactions)) + return true; + + if (st.isFieldPresent(sfBatchSigners) && + st.getFieldArray(sfBatchSigners).size() > maxBatchTxCount) + { + reason = "Batch Signers array exceeds max entries."; + return false; + } + + auto const& rawTxns = st.getFieldArray(sfRawTransactions); + if (rawTxns.size() > maxBatchTxCount) + { + reason = "Raw Transactions array exceeds max entries."; + return false; + } + for (STObject raw : rawTxns) + { + try + { + TxType const tt = + safe_cast(raw.getFieldU16(sfTransactionType)); + if (tt == ttBATCH) + { + reason = "Raw Transactions may not contain batch transactions."; + return false; + } + + raw.applyTemplate(getTxFormat(tt)->getSOTemplate()); + } + catch (std::exception const& e) + { + reason = e.what(); + return false; + } + } + return true; +} + bool passesLocalChecks(STObject const& st, std::string& reason) { @@ -630,6 +801,9 @@ passesLocalChecks(STObject const& st, std::string& reason) return false; } + if (!isRawTransactionOkay(st, reason)) + return false; + return true; } @@ -645,10 +819,13 @@ sterilize(STTx const& stx) bool isPseudoTx(STObject const& tx) { - auto t = tx[~sfTransactionType]; + auto const t = tx[~sfTransactionType]; + if (!t) return false; - auto tt = safe_cast(*t); + + auto const tt = safe_cast(*t); + return tt == ttAMENDMENT || tt == ttFEE || tt == ttUNL_MODIFY; } diff --git a/src/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp index 943a0e601b..a396949afe 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -126,6 +126,8 @@ transResults() MAKE_ERROR(tecWRONG_ASSET, "Wrong asset given."), MAKE_ERROR(tecLIMIT_EXCEEDED, "Limit exceeded."), MAKE_ERROR(tecPSEUDO_ACCOUNT, "This operation is not allowed against a pseudo-account."), + MAKE_ERROR(tecPRECISION_LOSS, "The amounts used by the transaction cannot interact."), + MAKE_ERROR(tecNO_DELEGATE_PERMISSION, "Delegated account lacks permission to perform this transaction."), MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."), MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."), @@ -217,6 +219,7 @@ transResults() MAKE_ERROR(temARRAY_EMPTY, "Malformed: Array is empty."), MAKE_ERROR(temARRAY_TOO_LARGE, "Malformed: Array is too large."), MAKE_ERROR(temBAD_TRANSFER_FEE, "Malformed: Transfer fee is outside valid range."), + MAKE_ERROR(temINVALID_INNER_BATCH, "Malformed: Invalid inner batch transaction."), MAKE_ERROR(terRETRY, "Retry transaction."), MAKE_ERROR(terFUNDS_SPENT, "DEPRECATED."), diff --git a/src/libxrpl/protocol/TxMeta.cpp b/src/libxrpl/protocol/TxMeta.cpp index d9a9f0db87..2343a6a794 100644 --- a/src/libxrpl/protocol/TxMeta.cpp +++ b/src/libxrpl/protocol/TxMeta.cpp @@ -56,6 +56,9 @@ TxMeta::TxMeta( if (obj.isFieldPresent(sfDeliveredAmount)) setDeliveredAmount(obj.getFieldAmount(sfDeliveredAmount)); + + if (obj.isFieldPresent(sfParentBatchID)) + setParentBatchId(obj.getFieldH256(sfParentBatchID)); } TxMeta::TxMeta(uint256 const& txid, std::uint32_t ledger, STObject const& obj) @@ -76,6 +79,9 @@ TxMeta::TxMeta(uint256 const& txid, std::uint32_t ledger, STObject const& obj) if (obj.isFieldPresent(sfDeliveredAmount)) setDeliveredAmount(obj.getFieldAmount(sfDeliveredAmount)); + + if (obj.isFieldPresent(sfParentBatchID)) + setParentBatchId(obj.getFieldH256(sfParentBatchID)); } TxMeta::TxMeta(uint256 const& txid, std::uint32_t ledger, Blob const& vec) @@ -91,11 +97,15 @@ TxMeta::TxMeta( { } -TxMeta::TxMeta(uint256 const& transactionID, std::uint32_t ledger) +TxMeta::TxMeta( + uint256 const& transactionID, + std::uint32_t ledger, + std::optional parentBatchId) : mTransactionID(transactionID) , mLedger(ledger) , mIndex(static_cast(-1)) , mResult(255) + , mParentBatchId(parentBatchId) , mNodes(sfAffectedNodes) { mNodes.reserve(32); @@ -175,6 +185,18 @@ TxMeta::getAffectedAccounts() const { auto issuer = lim->getIssuer(); + if (issuer.isNonZero()) + list.insert(issuer); + } + } + else if (field.getFName() == sfMPTokenIssuanceID) + { + auto mptID = + dynamic_cast const*>(&field); + if (mptID != nullptr) + { + auto issuer = MPTIssue(mptID->value()).getIssuer(); + if (issuer.isNonZero()) list.insert(issuer); } @@ -231,6 +253,10 @@ TxMeta::getAsObject() const metaData.emplace_back(mNodes); if (hasDeliveredAmount()) metaData.setFieldAmount(sfDeliveredAmount, getDeliveredAmount()); + + if (hasParentBatchId()) + metaData.setFieldH256(sfParentBatchID, getParentBatchId()); + return metaData; } diff --git a/src/test/app/AMMClawback_test.cpp b/src/test/app/AMMClawback_test.cpp index 878c570a12..9564911664 100644 --- a/src/test/app/AMMClawback_test.cpp +++ b/src/test/app/AMMClawback_test.cpp @@ -17,26 +17,25 @@ #include #include -#include -#include +#include + +#include #include -#include - namespace ripple { namespace test { -class AMMClawback_test : public jtx::AMMTest +class AMMClawback_test : public beast::unit_test::suite { void - testInvalidRequest(FeatureBitset features) + testInvalidRequest() { testcase("test invalid request"); using namespace jtx; // Test if holder does not exist. { - Env env(*this, features); + Env env(*this); Account gw{"gateway"}; Account alice{"alice"}; env.fund(XRP(100000), gw, alice); @@ -47,8 +46,9 @@ class AMMClawback_test : public jtx::AMMTest env.close(); env.require(flags(gw, asfAllowTrustLineClawback)); + auto const USD = gw["USD"]; env.trust(USD(10000), alice); - env(pay(gw, alice, gw["USD"](100))); + env(pay(gw, alice, USD(100))); AMM amm(env, alice, XRP(100), USD(100)); env.close(); @@ -61,7 +61,7 @@ class AMMClawback_test : public jtx::AMMTest // Test if asset pair provided does not exist. This should // return terNO_AMM error. { - Env env(*this, features); + Env env(*this); Account gw{"gateway"}; Account alice{"alice"}; env.fund(XRP(100000), gw, alice); @@ -87,14 +87,14 @@ class AMMClawback_test : public jtx::AMMTest // The AMM account does not exist at all now. // It should return terNO_AMM error. - env(amm::ammClawback(gw, alice, USD, EUR, std::nullopt), + env(amm::ammClawback(gw, alice, USD, gw["EUR"], std::nullopt), ter(terNO_AMM)); } // Test if the issuer field and holder field is the same. This should // return temMALFORMED error. { - Env env(*this, features); + Env env(*this); Account gw{"gateway"}; Account alice{"alice"}; env.fund(XRP(10000), gw, alice); @@ -124,7 +124,7 @@ class AMMClawback_test : public jtx::AMMTest // Test if the Asset field matches the Account field. { - Env env(*this, features); + Env env(*this); Account gw{"gateway"}; Account alice{"alice"}; env.fund(XRP(10000), gw, alice); @@ -156,7 +156,7 @@ class AMMClawback_test : public jtx::AMMTest // Test if the Amount field matches the Asset field. { - Env env(*this, features); + Env env(*this); Account gw{"gateway"}; Account alice{"alice"}; env.fund(XRP(10000), gw, alice); @@ -189,7 +189,7 @@ class AMMClawback_test : public jtx::AMMTest // Test if the Amount is invalid, which is less than zero. { - Env env(*this, features); + Env env(*this); Account gw{"gateway"}; Account alice{"alice"}; env.fund(XRP(10000), gw, alice); @@ -230,7 +230,7 @@ class AMMClawback_test : public jtx::AMMTest // Test if the issuer did not set asfAllowTrustLineClawback, AMMClawback // transaction is prohibited. { - Env env(*this, features); + Env env(*this); Account gw{"gateway"}; Account alice{"alice"}; env.fund(XRP(10000), gw, alice); @@ -241,7 +241,7 @@ class AMMClawback_test : public jtx::AMMTest env.trust(USD(1000), alice); env(pay(gw, alice, USD(100))); env.close(); - env.require(balance(alice, gw["USD"](100))); + env.require(balance(alice, USD(100))); env.require(balance(gw, alice["USD"](-100))); // gw creates AMM pool of XRP/USD. @@ -255,7 +255,7 @@ class AMMClawback_test : public jtx::AMMTest // Test invalid flag. { - Env env(*this, features); + Env env(*this); Account gw{"gateway"}; Account alice{"alice"}; env.fund(XRP(10000), gw, alice); @@ -283,7 +283,7 @@ class AMMClawback_test : public jtx::AMMTest // Test if tfClawTwoAssets is set when the two assets in the AMM pool // are not issued by the same issuer. { - Env env(*this, features); + Env env(*this); Account gw{"gateway"}; Account alice{"alice"}; env.fund(XRP(10000), gw, alice); @@ -314,7 +314,7 @@ class AMMClawback_test : public jtx::AMMTest // Test clawing back XRP is being prohibited. { - Env env(*this, features); + Env env(*this); Account gw{"gateway"}; Account alice{"alice"}; env.fund(XRP(1000000), gw, alice); @@ -400,7 +400,7 @@ class AMMClawback_test : public jtx::AMMTest env(pay(gw, alice, USD(3000))); env.close(); env.require(balance(gw, alice["USD"](-3000))); - env.require(balance(alice, gw["USD"](3000))); + env.require(balance(alice, USD(3000))); // gw2 issues 3000 EUR to Alice. auto const EUR = gw2["EUR"]; @@ -408,7 +408,7 @@ class AMMClawback_test : public jtx::AMMTest env(pay(gw2, alice, EUR(3000))); env.close(); env.require(balance(gw2, alice["EUR"](-3000))); - env.require(balance(alice, gw2["EUR"](3000))); + env.require(balance(alice, EUR(3000))); // Alice creates AMM pool of EUR/USD. AMM amm(env, alice, EUR(1000), USD(2000), ter(tesSUCCESS)); @@ -426,13 +426,13 @@ class AMMClawback_test : public jtx::AMMTest // USD into the pool, then she has 1000 USD. And 1000 USD was clawed // back from the AMM pool, so she still has 1000 USD. env.require(balance(gw, alice["USD"](-1000))); - env.require(balance(alice, gw["USD"](1000))); + env.require(balance(alice, USD(1000))); // Alice's initial balance for EUR is 3000 EUR. Alice deposited 1000 // EUR into the pool, 500 EUR was withdrawn proportionally. So she // has 2500 EUR now. env.require(balance(gw2, alice["EUR"](-2500))); - env.require(balance(alice, gw2["EUR"](2500))); + env.require(balance(alice, EUR(2500))); // 1000 USD and 500 EUR was withdrawn from the AMM pool, so the // current balance is 1000 USD and 500 EUR. @@ -452,12 +452,12 @@ class AMMClawback_test : public jtx::AMMTest // Alice should still has 1000 USD because gw clawed back from the // AMM pool. env.require(balance(gw, alice["USD"](-1000))); - env.require(balance(alice, gw["USD"](1000))); + env.require(balance(alice, USD(1000))); // Alice should has 3000 EUR now because another 500 EUR was // withdrawn. env.require(balance(gw2, alice["EUR"](-3000))); - env.require(balance(alice, gw2["EUR"](3000))); + env.require(balance(alice, EUR(3000))); // amm is automatically deleted. BEAST_EXPECT(!amm.ammExists()); @@ -483,7 +483,7 @@ class AMMClawback_test : public jtx::AMMTest env(pay(gw, alice, USD(3000))); env.close(); env.require(balance(gw, alice["USD"](-3000))); - env.require(balance(alice, gw["USD"](3000))); + env.require(balance(alice, USD(3000))); // Alice creates AMM pool of XRP/USD. AMM amm(env, alice, XRP(1000), USD(2000), ter(tesSUCCESS)); @@ -503,11 +503,12 @@ class AMMClawback_test : public jtx::AMMTest // USD into the pool, then she has 1000 USD. And 1000 USD was clawed // back from the AMM pool, so she still has 1000 USD. env.require(balance(gw, alice["USD"](-1000))); - env.require(balance(alice, gw["USD"](1000))); + env.require(balance(alice, USD(1000))); // Alice will get 500 XRP back. BEAST_EXPECT( expectLedgerEntryRoot(env, alice, aliceXrpBalance + XRP(500))); + aliceXrpBalance = env.balance(alice, XRP); // 1000 USD and 500 XRP was withdrawn from the AMM pool, so the // current balance is 1000 USD and 500 XRP. @@ -527,11 +528,11 @@ class AMMClawback_test : public jtx::AMMTest // Alice should still has 1000 USD because gw clawed back from the // AMM pool. env.require(balance(gw, alice["USD"](-1000))); - env.require(balance(alice, gw["USD"](1000))); + env.require(balance(alice, USD(1000))); - // Alice will get another 1000 XRP back. + // Alice will get another 500 XRP back. BEAST_EXPECT( - expectLedgerEntryRoot(env, alice, aliceXrpBalance + XRP(1000))); + expectLedgerEntryRoot(env, alice, aliceXrpBalance + XRP(500))); // amm is automatically deleted. BEAST_EXPECT(!amm.ammExists()); @@ -568,21 +569,25 @@ class AMMClawback_test : public jtx::AMMTest env.trust(USD(100000), alice); env(pay(gw, alice, USD(6000))); env.close(); - env.require(balance(alice, gw["USD"](6000))); + env.require(balance(alice, USD(6000))); // gw2 issues 6000 EUR to Alice. auto const EUR = gw2["EUR"]; env.trust(EUR(100000), alice); env(pay(gw2, alice, EUR(6000))); env.close(); - env.require(balance(alice, gw2["EUR"](6000))); + env.require(balance(alice, EUR(6000))); // Alice creates AMM pool of EUR/USD AMM amm(env, alice, EUR(5000), USD(4000), ter(tesSUCCESS)); env.close(); - BEAST_EXPECT(amm.expectBalances( - USD(4000), EUR(5000), IOUAmount{4472135954999580, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + USD(4000), EUR(5000), IOUAmount{4472135954999580, -12})); + else + BEAST_EXPECT(amm.expectBalances( + USD(4000), EUR(5000), IOUAmount{4472135954999579, -12})); // gw clawback 1000 USD from the AMM pool env(amm::ammClawback(gw, alice, USD, EUR, USD(1000)), @@ -592,21 +597,29 @@ class AMMClawback_test : public jtx::AMMTest // Alice's initial balance for USD is 6000 USD. Alice deposited 4000 // USD into the pool, then she has 2000 USD. And 1000 USD was clawed // back from the AMM pool, so she still has 2000 USD. - env.require(balance(alice, gw["USD"](2000))); + env.require(balance(alice, USD(2000))); // Alice's initial balance for EUR is 6000 EUR. Alice deposited 5000 // EUR into the pool, 1250 EUR was withdrawn proportionally. So she // has 2500 EUR now. - env.require(balance(alice, gw2["EUR"](2250))); + env.require(balance(alice, EUR(2250))); // 1000 USD and 1250 EUR was withdrawn from the AMM pool, so the // current balance is 3000 USD and 3750 EUR. - BEAST_EXPECT(amm.expectBalances( - USD(3000), EUR(3750), IOUAmount{3354101966249685, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + USD(3000), EUR(3750), IOUAmount{3354101966249685, -12})); + else + BEAST_EXPECT(amm.expectBalances( + USD(3000), EUR(3750), IOUAmount{3354101966249684, -12})); // Alice has 3/4 of its initial lptokens Left. - BEAST_EXPECT( - amm.expectLPTokens(alice, IOUAmount{3354101966249685, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectLPTokens( + alice, IOUAmount{3354101966249685, -12})); + else + BEAST_EXPECT(amm.expectLPTokens( + alice, IOUAmount{3354101966249684, -12})); // gw clawback another 500 USD from the AMM pool. env(amm::ammClawback(gw, alice, USD, EUR, USD(500)), @@ -615,32 +628,55 @@ class AMMClawback_test : public jtx::AMMTest // Alice should still has 2000 USD because gw clawed back from the // AMM pool. - env.require(balance(alice, gw["USD"](2000))); + env.require(balance(alice, USD(2000))); - BEAST_EXPECT(amm.expectBalances( - STAmount{USD, UINT64_C(2500000000000001), -12}, - STAmount{EUR, UINT64_C(3125000000000001), -12}, - IOUAmount{2795084971874738, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + STAmount{USD, UINT64_C(2500000000000001), -12}, + STAmount{EUR, UINT64_C(3125000000000001), -12}, + IOUAmount{2795084971874738, -12})); + else + BEAST_EXPECT(amm.expectBalances( + USD(2500), EUR(3125), IOUAmount{2795084971874737, -12})); - BEAST_EXPECT( - env.balance(alice, EUR) == - STAmount(EUR, UINT64_C(2874999999999999), -12)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT( + env.balance(alice, EUR) == + STAmount(EUR, UINT64_C(2874999999999999), -12)); + else + BEAST_EXPECT(env.balance(alice, EUR) == EUR(2875)); // gw clawback small amount, 1 USD. env(amm::ammClawback(gw, alice, USD, EUR, USD(1)), ter(tesSUCCESS)); env.close(); // Another 1 USD / 1.25 EUR was withdrawn. - env.require(balance(alice, gw["USD"](2000))); + env.require(balance(alice, USD(2000))); - BEAST_EXPECT(amm.expectBalances( - STAmount{USD, UINT64_C(2499000000000002), -12}, - STAmount{EUR, UINT64_C(3123750000000002), -12}, - IOUAmount{2793966937885989, -12})); + if (!features[fixAMMv1_3] && !features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm.expectBalances( + STAmount{USD, UINT64_C(2499000000000002), -12}, + STAmount{EUR, UINT64_C(3123750000000002), -12}, + IOUAmount{2793966937885989, -12})); + else if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm.expectBalances( + USD(2499), EUR(3123.75), IOUAmount{2793966937885987, -12})); + else if (features[fixAMMClawbackRounding] && features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + STAmount{USD, UINT64_C(2499000000000001), -12}, + STAmount{EUR, UINT64_C(3123750000000001), -12}, + IOUAmount{2793966937885988, -12})); - BEAST_EXPECT( - env.balance(alice, EUR) == - STAmount(EUR, UINT64_C(2876249999999998), -12)); + if (!features[fixAMMv1_3] && !features[fixAMMClawbackRounding]) + BEAST_EXPECT( + env.balance(alice, EUR) == + STAmount(EUR, UINT64_C(2876'249999999998), -12)); + else if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT(env.balance(alice, EUR) == EUR(2876.25)); + else if (features[fixAMMClawbackRounding] && features[fixAMMv1_3]) + BEAST_EXPECT( + env.balance(alice, EUR) == + STAmount(EUR, UINT64_C(2876'249999999999), -12)); // gw clawback 4000 USD, exceeding the current balance. We // will clawback all. @@ -648,7 +684,7 @@ class AMMClawback_test : public jtx::AMMTest ter(tesSUCCESS)); env.close(); - env.require(balance(alice, gw["USD"](2000))); + env.require(balance(alice, USD(2000))); // All alice's EUR in the pool goes back to alice. BEAST_EXPECT( @@ -713,14 +749,28 @@ class AMMClawback_test : public jtx::AMMTest // gw2 creates AMM pool of XRP/EUR, alice and bob deposit XRP/EUR. AMM amm2(env, gw2, XRP(3000), EUR(1000), ter(tesSUCCESS)); - BEAST_EXPECT(amm2.expectBalances( - EUR(1000), XRP(3000), IOUAmount{1732050807568878, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm2.expectBalances( + EUR(1000), XRP(3000), IOUAmount{1732050807568878, -9})); + else + BEAST_EXPECT(amm2.expectBalances( + EUR(1000), XRP(3000), IOUAmount{1732050807568877, -9})); + amm2.deposit(alice, EUR(1000), XRP(3000)); - BEAST_EXPECT(amm2.expectBalances( - EUR(2000), XRP(6000), IOUAmount{3464101615137756, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm2.expectBalances( + EUR(2000), XRP(6000), IOUAmount{3464101615137756, -9})); + else + BEAST_EXPECT(amm2.expectBalances( + EUR(2000), XRP(6000), IOUAmount{3464101615137754, -9})); + amm2.deposit(bob, EUR(1000), XRP(3000)); - BEAST_EXPECT(amm2.expectBalances( - EUR(3000), XRP(9000), IOUAmount{5196152422706634, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm2.expectBalances( + EUR(3000), XRP(9000), IOUAmount{5196152422706634, -9})); + else + BEAST_EXPECT(amm2.expectBalances( + EUR(3000), XRP(9000), IOUAmount{5196152422706631, -9})); env.close(); auto aliceXrpBalance = env.balance(alice, XRP); @@ -734,19 +784,42 @@ class AMMClawback_test : public jtx::AMMTest // Alice's initial balance for USD is 6000 USD. Alice deposited 1000 // USD into the pool, then she has 5000 USD. And 500 USD was clawed // back from the AMM pool, so she still has 5000 USD. - env.require(balance(alice, gw["USD"](5000))); + env.require(balance(alice, USD(5000))); // Bob's balance is not changed. - env.require(balance(bob, gw["USD"](4000))); + env.require(balance(bob, USD(4000))); // Alice gets 1000 XRP back. - BEAST_EXPECT( - expectLedgerEntryRoot(env, alice, aliceXrpBalance + XRP(1000))); + if (features[fixAMMClawbackRounding] && features[fixAMMv1_3]) + BEAST_EXPECT(expectLedgerEntryRoot( + env, alice, aliceXrpBalance + XRP(1000) - XRPAmount(1))); + else + BEAST_EXPECT(expectLedgerEntryRoot( + env, alice, aliceXrpBalance + XRP(1000))); + aliceXrpBalance = env.balance(alice, XRP); + + if (!features[fixAMMv1_3] && !features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm.expectBalances( + USD(2500), XRP(5000), IOUAmount{3535533905932738, -9})); + else if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm.expectBalances( + USD(2500), XRP(5000), IOUAmount{3535533905932737, -9})); + else if (features[fixAMMClawbackRounding] && features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + USD(2500), + XRPAmount(5000000001), + IOUAmount{3'535'533'905932738, -9})); + + if (!features[fixAMMv1_3] && !features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm.expectLPTokens( + alice, IOUAmount{7071067811865480, -10})); + else if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm.expectLPTokens( + alice, IOUAmount{7071067811865474, -10})); + else if (features[fixAMMClawbackRounding] && features[fixAMMv1_3]) + BEAST_EXPECT( + amm.expectLPTokens(alice, IOUAmount{707106781186548, -9})); - BEAST_EXPECT(amm.expectBalances( - USD(2500), XRP(5000), IOUAmount{3535533905932738, -9})); - BEAST_EXPECT( - amm.expectLPTokens(alice, IOUAmount{7071067811865480, -10})); BEAST_EXPECT( amm.expectLPTokens(bob, IOUAmount{1414213562373095, -9})); @@ -754,38 +827,91 @@ class AMMClawback_test : public jtx::AMMTest env(amm::ammClawback(gw, bob, USD, XRP, USD(10)), ter(tesSUCCESS)); env.close(); - env.require(balance(alice, gw["USD"](5000))); - env.require(balance(bob, gw["USD"](4000))); + env.require(balance(alice, USD(5000))); + env.require(balance(bob, USD(4000))); // Bob gets 20 XRP back. BEAST_EXPECT( expectLedgerEntryRoot(env, bob, bobXrpBalance + XRP(20))); - BEAST_EXPECT(amm.expectBalances( - STAmount{USD, UINT64_C(2490000000000001), -12}, - XRP(4980), - IOUAmount{3521391770309008, -9})); - BEAST_EXPECT( - amm.expectLPTokens(alice, IOUAmount{7071067811865480, -10})); - BEAST_EXPECT( - amm.expectLPTokens(bob, IOUAmount{1400071426749365, -9})); + bobXrpBalance = env.balance(bob, XRP); + + if (!features[fixAMMv1_3] && !features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm.expectBalances( + STAmount{USD, UINT64_C(2490000000000001), -12}, + XRP(4980), + IOUAmount{3521391770309008, -9})); + else if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm.expectBalances( + USD(2'490), XRP(4980), IOUAmount{3521391770309006, -9})); + else if (features[fixAMMClawbackRounding] && features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + STAmount{USD, UINT64_C(2490000000000001), -12}, + XRPAmount(4980000001), + IOUAmount{3521391'770309008, -9})); + + if (!features[fixAMMv1_3] && !features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm.expectLPTokens( + alice, IOUAmount{7071067811865480, -10})); + else if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm.expectLPTokens( + alice, IOUAmount{7071067811865474, -10})); + else if (features[fixAMMClawbackRounding] && features[fixAMMv1_3]) + BEAST_EXPECT( + amm.expectLPTokens(alice, IOUAmount{707106781186548, -9})); + + if (!features[fixAMMv1_3] && !features[fixAMMClawbackRounding]) + BEAST_EXPECT( + amm.expectLPTokens(bob, IOUAmount{1400071426749365, -9})); + else if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT( + amm.expectLPTokens(bob, IOUAmount{1400071426749364, -9})); + else if (features[fixAMMClawbackRounding] && features[fixAMMv1_3]) + BEAST_EXPECT( + amm.expectLPTokens(bob, IOUAmount{1400071426749365, -9})); // gw2 clawback 200 EUR from amm2. env(amm::ammClawback(gw2, alice, EUR, XRP, EUR(200)), ter(tesSUCCESS)); env.close(); - env.require(balance(alice, gw2["EUR"](4000))); - env.require(balance(bob, gw2["EUR"](3000))); + env.require(balance(alice, EUR(4000))); + env.require(balance(bob, EUR(3000))); - // Alice gets 600 XRP back. - BEAST_EXPECT(expectLedgerEntryRoot( - env, alice, aliceXrpBalance + XRP(1000) + XRP(600))); - BEAST_EXPECT(amm2.expectBalances( - EUR(2800), XRP(8400), IOUAmount{4849742261192859, -9})); - BEAST_EXPECT( - amm2.expectLPTokens(alice, IOUAmount{1385640646055103, -9})); - BEAST_EXPECT( - amm2.expectLPTokens(bob, IOUAmount{1732050807568878, -9})); + if (!features[fixAMMv1_3] && !features[fixAMMClawbackRounding]) + BEAST_EXPECT(expectLedgerEntryRoot( + env, alice, aliceXrpBalance + XRP(600))); + else if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT(expectLedgerEntryRoot( + env, alice, aliceXrpBalance + XRP(600))); + else if (features[fixAMMClawbackRounding] && features[fixAMMv1_3]) + BEAST_EXPECT(expectLedgerEntryRoot( + env, alice, aliceXrpBalance + XRP(600) - XRPAmount{1})); + aliceXrpBalance = env.balance(alice, XRP); + + if (!features[fixAMMv1_3] && !features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm2.expectBalances( + EUR(2800), XRP(8400), IOUAmount{4849742261192859, -9})); + else if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm2.expectBalances( + EUR(2800), XRP(8400), IOUAmount{4849742261192856, -9})); + else if (features[fixAMMv1_3] && features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm2.expectBalances( + EUR(2800), + XRPAmount(8400000001), + IOUAmount{4849742261192856, -9})); + + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm2.expectLPTokens( + alice, IOUAmount{1385640646055103, -9})); + else + BEAST_EXPECT(amm2.expectLPTokens( + alice, IOUAmount{1385640646055102, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT( + amm2.expectLPTokens(bob, IOUAmount{1732050807568878, -9})); + else + BEAST_EXPECT( + amm2.expectLPTokens(bob, IOUAmount{1732050807568877, -9})); // gw claw back 1000 USD from alice in amm, which exceeds alice's // balance. This will clawback all the remaining LP tokens of alice @@ -794,21 +920,47 @@ class AMMClawback_test : public jtx::AMMTest ter(tesSUCCESS)); env.close(); - env.require(balance(alice, gw["USD"](5000))); - env.require(balance(bob, gw["USD"](4000))); + env.require(balance(alice, USD(5000))); + env.require(balance(bob, USD(4000))); // Alice gets 1000 XRP back. - BEAST_EXPECT(expectLedgerEntryRoot( - env, - alice, - aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000))); + if (!features[fixAMMv1_3] && !features[fixAMMClawbackRounding]) + BEAST_EXPECT(expectLedgerEntryRoot( + env, alice, aliceXrpBalance + XRP(1000))); + else if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT(expectLedgerEntryRoot( + env, alice, aliceXrpBalance + XRP(1000) - XRPAmount{1})); + else if (features[fixAMMv1_3] && features[fixAMMClawbackRounding]) + BEAST_EXPECT(expectLedgerEntryRoot( + env, alice, aliceXrpBalance + XRP(1000))); + aliceXrpBalance = env.balance(alice, XRP); + BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0))); - BEAST_EXPECT( - amm.expectLPTokens(bob, IOUAmount{1400071426749365, -9})); - BEAST_EXPECT(amm.expectBalances( - STAmount{USD, UINT64_C(1990000000000001), -12}, - XRP(3980), - IOUAmount{2814284989122460, -9})); + if (!features[fixAMMv1_3] && !features[fixAMMClawbackRounding]) + BEAST_EXPECT( + amm.expectLPTokens(bob, IOUAmount{1400071426749365, -9})); + else if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT( + amm.expectLPTokens(bob, IOUAmount{1400071426749364, -9})); + else if (features[fixAMMClawbackRounding] && features[fixAMMv1_3]) + BEAST_EXPECT( + amm.expectLPTokens(bob, IOUAmount{1400071426749365, -9})); + + if (!features[fixAMMv1_3] && !features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm.expectBalances( + STAmount{USD, UINT64_C(1990000000000001), -12}, + XRP(3980), + IOUAmount{2814284989122460, -9})); + else if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm.expectBalances( + USD(1'990), + XRPAmount{3'980'000'001}, + IOUAmount{2814284989122459, -9})); + else if (features[fixAMMv1_3] && features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm.expectBalances( + STAmount{USD, UINT64_C(1990000000000001), -12}, + XRPAmount{3'980'000'001}, + IOUAmount{2814284989122460, -9})); // gw clawback 1000 USD from bob in amm, which also exceeds bob's // balance in amm. All bob's lptoken in amm will be consumed, which @@ -817,15 +969,14 @@ class AMMClawback_test : public jtx::AMMTest ter(tesSUCCESS)); env.close(); - env.require(balance(alice, gw["USD"](5000))); - env.require(balance(bob, gw["USD"](4000))); + env.require(balance(alice, USD(5000))); + env.require(balance(bob, USD(4000))); - BEAST_EXPECT(expectLedgerEntryRoot( - env, - alice, - aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000))); - BEAST_EXPECT(expectLedgerEntryRoot( - env, bob, bobXrpBalance + XRP(20) + XRP(1980))); + BEAST_EXPECT(expectLedgerEntryRoot(env, alice, aliceXrpBalance)); + + BEAST_EXPECT( + expectLedgerEntryRoot(env, bob, bobXrpBalance + XRP(1980))); + bobXrpBalance = env.balance(bob, XRP); // Now neither alice nor bob has any lptoken in amm. BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0))); @@ -838,51 +989,65 @@ class AMMClawback_test : public jtx::AMMTest ter(tesSUCCESS)); env.close(); - env.require(balance(alice, gw2["EUR"](4000))); - env.require(balance(bob, gw2["EUR"](3000))); + env.require(balance(alice, EUR(4000))); + env.require(balance(bob, EUR(3000))); // Alice gets another 2400 XRP back, bob's XRP balance remains the // same. - BEAST_EXPECT(expectLedgerEntryRoot( - env, - alice, - aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) + - XRP(2400))); - BEAST_EXPECT(expectLedgerEntryRoot( - env, bob, bobXrpBalance + XRP(20) + XRP(1980))); + BEAST_EXPECT( + expectLedgerEntryRoot(env, alice, aliceXrpBalance + XRP(2400))); + + BEAST_EXPECT(expectLedgerEntryRoot(env, bob, bobXrpBalance)); + aliceXrpBalance = env.balance(alice, XRP); // Alice now does not have any lptoken in amm2 BEAST_EXPECT(amm2.expectLPTokens(alice, IOUAmount(0))); - BEAST_EXPECT(amm2.expectBalances( - EUR(2000), XRP(6000), IOUAmount{3464101615137756, -9})); + if (!features[fixAMMv1_3] && !features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm2.expectBalances( + EUR(2000), XRP(6000), IOUAmount{3464101615137756, -9})); + else if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm2.expectBalances( + EUR(2000), XRP(6000), IOUAmount{3464101615137754, -9})); + else if (features[fixAMMv1_3] && features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm2.expectBalances( + EUR(2000), + XRPAmount(6000000001), + IOUAmount{3464101615137754, -9})); - // gw2 claw back 2000 EUR from bib in amm2, which exceeds bob's + // gw2 claw back 2000 EUR from bob in amm2, which exceeds bob's // balance. All bob's lptokens will be consumed, which corresponds // to 1000EUR / 3000 XRP. env(amm::ammClawback(gw2, bob, EUR, XRP, EUR(2000)), ter(tesSUCCESS)); env.close(); - env.require(balance(alice, gw2["EUR"](4000))); - env.require(balance(bob, gw2["EUR"](3000))); + env.require(balance(alice, EUR(4000))); + env.require(balance(bob, EUR(3000))); // Bob gets another 3000 XRP back. Alice's XRP balance remains the // same. - BEAST_EXPECT(expectLedgerEntryRoot( - env, - alice, - aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) + - XRP(2400))); - BEAST_EXPECT(expectLedgerEntryRoot( - env, bob, bobXrpBalance + XRP(20) + XRP(1980) + XRP(3000))); + BEAST_EXPECT(expectLedgerEntryRoot(env, alice, aliceXrpBalance)); + + BEAST_EXPECT( + expectLedgerEntryRoot(env, bob, bobXrpBalance + XRP(3000))); + bobXrpBalance = env.balance(bob, XRP); // Neither alice nor bob has any lptoken in amm2 BEAST_EXPECT(amm2.expectLPTokens(alice, IOUAmount(0))); BEAST_EXPECT(amm2.expectLPTokens(bob, IOUAmount(0))); - BEAST_EXPECT(amm2.expectBalances( - EUR(1000), XRP(3000), IOUAmount{1732050807568878, -9})); + if (!features[fixAMMv1_3] && !features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm2.expectBalances( + EUR(1000), XRP(3000), IOUAmount{1732050807568878, -9})); + else if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm2.expectBalances( + EUR(1000), XRP(3000), IOUAmount{1732050807568877, -9})); + else if (features[fixAMMv1_3] && features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm2.expectBalances( + EUR(1000), + XRPAmount(3000000001), + IOUAmount{1732050807568877, -9})); } } @@ -940,69 +1105,122 @@ class AMMClawback_test : public jtx::AMMTest AMM amm(env, alice, EUR(5000), USD(4000), ter(tesSUCCESS)); env.close(); - BEAST_EXPECT(amm.expectBalances( - USD(4000), EUR(5000), IOUAmount{4472135954999580, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + USD(4000), EUR(5000), IOUAmount{4472135954999580, -12})); + else + BEAST_EXPECT(amm.expectBalances( + USD(4000), EUR(5000), IOUAmount{4472135954999579, -12})); amm.deposit(bob, USD(2000), EUR(2500)); - BEAST_EXPECT(amm.expectBalances( - USD(6000), EUR(7500), IOUAmount{6708203932499370, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + USD(6000), EUR(7500), IOUAmount{6708203932499370, -12})); + else + BEAST_EXPECT(amm.expectBalances( + USD(6000), EUR(7500), IOUAmount{6708203932499368, -12})); amm.deposit(carol, USD(1000), EUR(1250)); - BEAST_EXPECT(amm.expectBalances( - USD(7000), EUR(8750), IOUAmount{7826237921249265, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + USD(7000), EUR(8750), IOUAmount{7826237921249265, -12})); + else + BEAST_EXPECT(amm.expectBalances( + USD(7000), EUR(8750), IOUAmount{7826237921249262, -12})); - BEAST_EXPECT( - amm.expectLPTokens(alice, IOUAmount{4472135954999580, -12})); - BEAST_EXPECT( - amm.expectLPTokens(bob, IOUAmount{2236067977499790, -12})); - BEAST_EXPECT( - amm.expectLPTokens(carol, IOUAmount{1118033988749895, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectLPTokens( + alice, IOUAmount{4472135954999580, -12})); + else + BEAST_EXPECT(amm.expectLPTokens( + alice, IOUAmount{4472135954999579, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT( + amm.expectLPTokens(bob, IOUAmount{2236067977499790, -12})); + else + BEAST_EXPECT( + amm.expectLPTokens(bob, IOUAmount{2236067977499789, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectLPTokens( + carol, IOUAmount{1118033988749895, -12})); + else + BEAST_EXPECT(amm.expectLPTokens( + carol, IOUAmount{1118033988749894, -12})); - env.require(balance(alice, gw["USD"](2000))); - env.require(balance(alice, gw2["EUR"](1000))); - env.require(balance(bob, gw["USD"](3000))); - env.require(balance(bob, gw2["EUR"](2500))); - env.require(balance(carol, gw["USD"](3000))); - env.require(balance(carol, gw2["EUR"](2750))); + env.require(balance(alice, USD(2000))); + env.require(balance(alice, EUR(1000))); + env.require(balance(bob, USD(3000))); + env.require(balance(bob, EUR(2500))); + env.require(balance(carol, USD(3000))); + env.require(balance(carol, EUR(2750))); // gw clawback all the bob's USD in amm. (2000 USD / 2500 EUR) env(amm::ammClawback(gw, bob, USD, EUR, std::nullopt), ter(tesSUCCESS)); env.close(); - BEAST_EXPECT(amm.expectBalances( - STAmount{USD, UINT64_C(4999999999999999), -12}, - STAmount{EUR, UINT64_C(6249999999999999), -12}, - IOUAmount{5590169943749475, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + STAmount{USD, UINT64_C(4999999999999999), -12}, + STAmount{EUR, UINT64_C(6249999999999999), -12}, + IOUAmount{5590169943749475, -12})); + else + BEAST_EXPECT(amm.expectBalances( + STAmount{USD, UINT64_C(5000000000000001), -12}, + STAmount{EUR, UINT64_C(6250000000000001), -12}, + IOUAmount{5590169943749473, -12})); - BEAST_EXPECT( - amm.expectLPTokens(alice, IOUAmount{4472135954999580, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectLPTokens( + alice, IOUAmount{4472135954999580, -12})); + else + BEAST_EXPECT(amm.expectLPTokens( + alice, IOUAmount{4472135954999579, -12})); BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount(0))); - BEAST_EXPECT( - amm.expectLPTokens(carol, IOUAmount{1118033988749895, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectLPTokens( + carol, IOUAmount{1118033988749895, -12})); + else + BEAST_EXPECT(amm.expectLPTokens( + carol, IOUAmount{1118033988749894, -12})); // Bob will get 2500 EUR back. - env.require(balance(alice, gw["USD"](2000))); - env.require(balance(alice, gw2["EUR"](1000))); + env.require(balance(alice, USD(2000))); + env.require(balance(alice, EUR(1000))); BEAST_EXPECT( env.balance(bob, USD) == STAmount(USD, UINT64_C(3000000000000000), -12)); - BEAST_EXPECT( - env.balance(bob, EUR) == - STAmount(EUR, UINT64_C(5000000000000001), -12)); - env.require(balance(carol, gw["USD"](3000))); - env.require(balance(carol, gw2["EUR"](2750))); + if (!features[fixAMMv1_3]) + BEAST_EXPECT( + env.balance(bob, EUR) == + STAmount(EUR, UINT64_C(5000000000000001), -12)); + else + BEAST_EXPECT( + env.balance(bob, EUR) == + STAmount(EUR, UINT64_C(4999999999999999), -12)); + env.require(balance(carol, USD(3000))); + env.require(balance(carol, EUR(2750))); // gw2 clawback all carol's EUR in amm. (1000 USD / 1250 EUR) env(amm::ammClawback(gw2, carol, EUR, USD, std::nullopt), ter(tesSUCCESS)); env.close(); - BEAST_EXPECT(amm.expectBalances( - STAmount{USD, UINT64_C(3999999999999999), -12}, - STAmount{EUR, UINT64_C(4999999999999999), -12}, - IOUAmount{4472135954999580, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + STAmount{USD, UINT64_C(3999999999999999), -12}, + STAmount{EUR, UINT64_C(4999999999999999), -12}, + IOUAmount{4472135954999580, -12})); + else + BEAST_EXPECT(amm.expectBalances( + STAmount{USD, UINT64_C(4000000000000001), -12}, + STAmount{EUR, UINT64_C(5000000000000002), -12}, + IOUAmount{4472135954999579, -12})); - BEAST_EXPECT( - amm.expectLPTokens(alice, IOUAmount{4472135954999580, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectLPTokens( + alice, IOUAmount{4472135954999580, -12})); + else + BEAST_EXPECT(amm.expectLPTokens( + alice, IOUAmount{4472135954999579, -12})); BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount(0))); BEAST_EXPECT(amm.expectLPTokens(carol, IOUAmount(0))); @@ -1011,8 +1229,8 @@ class AMMClawback_test : public jtx::AMMTest ter(tesSUCCESS)); env.close(); - env.require(balance(carol, gw2["EUR"](2750))); - env.require(balance(carol, gw["USD"](4000))); + env.require(balance(carol, EUR(2750))); + env.require(balance(carol, USD(4000))); BEAST_EXPECT(!amm.ammExists()); } @@ -1041,14 +1259,26 @@ class AMMClawback_test : public jtx::AMMTest // gw creates AMM pool of XRP/USD, alice and bob deposit XRP/USD. AMM amm(env, gw, XRP(2000), USD(10000), ter(tesSUCCESS)); - BEAST_EXPECT(amm.expectBalances( - USD(10000), XRP(2000), IOUAmount{4472135954999580, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + USD(10000), XRP(2000), IOUAmount{4472135954999580, -9})); + else + BEAST_EXPECT(amm.expectBalances( + USD(10000), XRP(2000), IOUAmount{4472135954999579, -9})); amm.deposit(alice, USD(1000), XRP(200)); - BEAST_EXPECT(amm.expectBalances( - USD(11000), XRP(2200), IOUAmount{4919349550499538, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + USD(11000), XRP(2200), IOUAmount{4919349550499538, -9})); + else + BEAST_EXPECT(amm.expectBalances( + USD(11000), XRP(2200), IOUAmount{4919349550499536, -9})); amm.deposit(bob, USD(2000), XRP(400)); - BEAST_EXPECT(amm.expectBalances( - USD(13000), XRP(2600), IOUAmount{5813776741499453, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + USD(13000), XRP(2600), IOUAmount{5813776741499453, -9})); + else + BEAST_EXPECT(amm.expectBalances( + USD(13000), XRP(2600), IOUAmount{5813776741499451, -9})); env.close(); auto aliceXrpBalance = env.balance(alice, XRP); @@ -1058,18 +1288,34 @@ class AMMClawback_test : public jtx::AMMTest env(amm::ammClawback(gw, alice, USD, XRP, std::nullopt), ter(tesSUCCESS)); env.close(); - BEAST_EXPECT(amm.expectBalances( - USD(12000), XRP(2400), IOUAmount{5366563145999495, -9})); - BEAST_EXPECT( - expectLedgerEntryRoot(env, alice, aliceXrpBalance + XRP(200))); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + USD(12000), XRP(2400), IOUAmount{5366563145999495, -9})); + else + BEAST_EXPECT(amm.expectBalances( + USD(12000), + XRPAmount(2400000001), + IOUAmount{5366563145999494, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(expectLedgerEntryRoot( + env, alice, aliceXrpBalance + XRP(200))); + else + BEAST_EXPECT(expectLedgerEntryRoot( + env, alice, aliceXrpBalance + XRP(200) - XRPAmount{1})); BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0))); // gw clawback all bob's USD in amm. (2000 USD / 400 XRP) env(amm::ammClawback(gw, bob, USD, XRP, std::nullopt), ter(tesSUCCESS)); env.close(); - BEAST_EXPECT(amm.expectBalances( - USD(10000), XRP(2000), IOUAmount{4472135954999580, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + USD(10000), XRP(2000), IOUAmount{4472135954999580, -9})); + else + BEAST_EXPECT(amm.expectBalances( + USD(10000), + XRPAmount(2000000001), + IOUAmount{4472135954999579, -9})); BEAST_EXPECT( expectLedgerEntryRoot(env, bob, bobXrpBalance + XRP(400))); BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0))); @@ -1125,10 +1371,12 @@ class AMMClawback_test : public jtx::AMMTest amm.deposit(bob, USD(4000), EUR(1000)); BEAST_EXPECT( amm.expectBalances(USD(12000), EUR(3000), IOUAmount(6000))); - amm.deposit(carol, USD(2000), EUR(500)); + if (!features[fixAMMv1_3]) + amm.deposit(carol, USD(2000), EUR(500)); + else + amm.deposit(carol, USD(2000.25), EUR(500)); BEAST_EXPECT( amm.expectBalances(USD(14000), EUR(3500), IOUAmount(7000))); - // gw clawback 1000 USD from carol. env(amm::ammClawback(gw, carol, USD, EUR, USD(1000)), ter(tesSUCCESS)); env.close(); @@ -1142,7 +1390,12 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT(env.balance(alice, EUR) == EUR(8000)); BEAST_EXPECT(env.balance(bob, USD) == USD(5000)); BEAST_EXPECT(env.balance(bob, EUR) == EUR(8000)); - BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + else + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(5999'999999999999), -12)); // 250 EUR goes back to carol. BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750)); @@ -1164,7 +1417,12 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT(env.balance(bob, USD) == USD(5000)); // 250 EUR did not go back to bob because tfClawTwoAssets is set. BEAST_EXPECT(env.balance(bob, EUR) == EUR(8000)); - BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + else + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(5999'999999999999), -12)); BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750)); // gw clawback all USD from alice and set tfClawTwoAssets. @@ -1181,7 +1439,12 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT(env.balance(alice, EUR) == EUR(8000)); BEAST_EXPECT(env.balance(bob, USD) == USD(5000)); BEAST_EXPECT(env.balance(bob, EUR) == EUR(8000)); - BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + else + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(5999'999999999999), -12)); BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750)); } @@ -1350,11 +1613,20 @@ class AMMClawback_test : public jtx::AMMTest // gw claws back 1000 USD from gw2. env(amm::ammClawback(gw, gw2, USD, EUR, USD(1000)), ter(tesSUCCESS)); env.close(); - BEAST_EXPECT(amm.expectBalances( - USD(5000), EUR(10000), IOUAmount{7071067811865475, -12})); + if (!features[fixAMMv1_3] || !features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm.expectBalances( + USD(5000), EUR(10000), IOUAmount{7071067811865475, -12})); + else + BEAST_EXPECT(amm.expectBalances( + USD(5000), EUR(10000), IOUAmount{7071067811865474, -12})); BEAST_EXPECT(amm.expectLPTokens(gw, IOUAmount{1414213562373095, -12})); - BEAST_EXPECT(amm.expectLPTokens(gw2, IOUAmount{1414213562373095, -12})); + if (!features[fixAMMv1_3] || !features[fixAMMClawbackRounding]) + BEAST_EXPECT( + amm.expectLPTokens(gw2, IOUAmount{1414213562373095, -12})); + else + BEAST_EXPECT( + amm.expectLPTokens(gw2, IOUAmount{1414213562373094, -12})); BEAST_EXPECT( amm.expectLPTokens(alice, IOUAmount{4242640687119285, -12})); @@ -1366,13 +1638,37 @@ class AMMClawback_test : public jtx::AMMTest // gw2 claws back 1000 EUR from gw. env(amm::ammClawback(gw2, gw, EUR, USD, EUR(1000)), ter(tesSUCCESS)); env.close(); - BEAST_EXPECT(amm.expectBalances( - USD(4500), - STAmount(EUR, UINT64_C(9000000000000001), -12), - IOUAmount{6363961030678928, -12})); + if (!features[fixAMMv1_3] && !features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm.expectBalances( + USD(4500), + STAmount(EUR, UINT64_C(9000000000000001), -12), + IOUAmount{6363961030678928, -12})); + else if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm.expectBalances( + USD(4500), EUR(9000), IOUAmount{6363961030678928, -12})); + else if (features[fixAMMv1_3] && features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm.expectBalances( + USD(4500), + STAmount(EUR, UINT64_C(9000000000000001), -12), + IOUAmount{6363961030678927, -12})); + + if (!features[fixAMMv1_3] && !features[fixAMMClawbackRounding]) + BEAST_EXPECT( + amm.expectLPTokens(gw, IOUAmount{7071067811865480, -13})); + else if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT( + amm.expectLPTokens(gw, IOUAmount{7071067811865475, -13})); + else if (features[fixAMMv1_3] && features[fixAMMClawbackRounding]) + BEAST_EXPECT( + amm.expectLPTokens(gw, IOUAmount{7071067811865480, -13})); + + if (!features[fixAMMv1_3] || !features[fixAMMClawbackRounding]) + BEAST_EXPECT( + amm.expectLPTokens(gw2, IOUAmount{1414213562373095, -12})); + else + BEAST_EXPECT( + amm.expectLPTokens(gw2, IOUAmount{1414213562373094, -12})); - BEAST_EXPECT(amm.expectLPTokens(gw, IOUAmount{7071067811865480, -13})); - BEAST_EXPECT(amm.expectLPTokens(gw2, IOUAmount{1414213562373095, -12})); BEAST_EXPECT( amm.expectLPTokens(alice, IOUAmount{4242640687119285, -12})); @@ -1384,13 +1680,36 @@ class AMMClawback_test : public jtx::AMMTest // gw2 claws back 4000 EUR from alice. env(amm::ammClawback(gw2, alice, EUR, USD, EUR(4000)), ter(tesSUCCESS)); env.close(); - BEAST_EXPECT(amm.expectBalances( - USD(2500), - STAmount(EUR, UINT64_C(5000000000000001), -12), - IOUAmount{3535533905932738, -12})); + if (!features[fixAMMv1_3] && !features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm.expectBalances( + USD(2500), + STAmount(EUR, UINT64_C(5000000000000001), -12), + IOUAmount{3535533905932738, -12})); + else if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm.expectBalances( + USD(2500), EUR(5000), IOUAmount{3535533905932738, -12})); + else if (features[fixAMMv1_3] && features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm.expectBalances( + USD(2500), + STAmount(EUR, UINT64_C(5000000000000001), -12), + IOUAmount{3535533905932737, -12})); - BEAST_EXPECT(amm.expectLPTokens(gw, IOUAmount{7071067811865480, -13})); - BEAST_EXPECT(amm.expectLPTokens(gw2, IOUAmount{1414213562373095, -12})); + if (!features[fixAMMv1_3] && !features[fixAMMClawbackRounding]) + BEAST_EXPECT( + amm.expectLPTokens(gw, IOUAmount{7071067811865480, -13})); + else if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT( + amm.expectLPTokens(gw, IOUAmount{7071067811865475, -13})); + else if (features[fixAMMv1_3] && features[fixAMMClawbackRounding]) + BEAST_EXPECT( + amm.expectLPTokens(gw, IOUAmount{7071067811865480, -13})); + + if (!features[fixAMMv1_3] || !features[fixAMMClawbackRounding]) + BEAST_EXPECT( + amm.expectLPTokens(gw2, IOUAmount{1414213562373095, -12})); + else + BEAST_EXPECT( + amm.expectLPTokens(gw2, IOUAmount{1414213562373094, -12})); BEAST_EXPECT( amm.expectLPTokens(alice, IOUAmount{1414213562373095, -12})); @@ -1457,14 +1776,14 @@ class AMMClawback_test : public jtx::AMMTest env.trust(USD(100000), alice); env(pay(gw, alice, USD(3000))); env.close(); - env.require(balance(alice, gw["USD"](3000))); + env.require(balance(alice, USD(3000))); // gw2 issues 3000 EUR to Alice. auto const EUR = gw2["EUR"]; env.trust(EUR(100000), alice); env(pay(gw2, alice, EUR(3000))); env.close(); - env.require(balance(alice, gw2["EUR"](3000))); + env.require(balance(alice, EUR(3000))); // Alice creates AMM pool of EUR/USD. AMM amm(env, alice, EUR(1000), USD(2000), ter(tesSUCCESS)); @@ -1482,8 +1801,8 @@ class AMMClawback_test : public jtx::AMMTest ter(tesSUCCESS)); env.close(); - env.require(balance(alice, gw["USD"](1000))); - env.require(balance(alice, gw2["EUR"](2500))); + env.require(balance(alice, USD(1000))); + env.require(balance(alice, EUR(2500))); BEAST_EXPECT(amm.expectBalances( USD(1000), EUR(500), IOUAmount{7071067811865475, -13})); @@ -1499,8 +1818,8 @@ class AMMClawback_test : public jtx::AMMTest // Alice should still has 1000 USD because gw clawed back from the // AMM pool. - env.require(balance(alice, gw["USD"](1000))); - env.require(balance(alice, gw2["EUR"](3000))); + env.require(balance(alice, USD(1000))); + env.require(balance(alice, EUR(3000))); // amm is automatically deleted. BEAST_EXPECT(!amm.ammExists()); @@ -1525,14 +1844,14 @@ class AMMClawback_test : public jtx::AMMTest env.trust(USD(100000), alice); env(pay(gw, alice, USD(3000))); env.close(); - env.require(balance(alice, gw["USD"](3000))); + env.require(balance(alice, USD(3000))); // gw2 issues 3000 EUR to Alice. auto const EUR = gw2["EUR"]; env.trust(EUR(100000), alice); env(pay(gw2, alice, EUR(3000))); env.close(); - env.require(balance(alice, gw2["EUR"](3000))); + env.require(balance(alice, EUR(3000))); // Alice creates AMM pool of EUR/USD. AMM amm(env, alice, EUR(1000), USD(2000), ter(tesSUCCESS)); @@ -1551,8 +1870,8 @@ class AMMClawback_test : public jtx::AMMTest ter(tesSUCCESS)); env.close(); - env.require(balance(alice, gw["USD"](1000))); - env.require(balance(alice, gw2["EUR"](2500))); + env.require(balance(alice, USD(1000))); + env.require(balance(alice, EUR(2500))); BEAST_EXPECT(amm.expectBalances( USD(1000), EUR(500), IOUAmount{7071067811865475, -13})); BEAST_EXPECT( @@ -1578,14 +1897,14 @@ class AMMClawback_test : public jtx::AMMTest env.trust(USD(100000), alice); env(pay(gw, alice, USD(3000))); env.close(); - env.require(balance(alice, gw["USD"](3000))); + env.require(balance(alice, USD(3000))); // gw2 issues 3000 EUR to Alice. auto const EUR = gw2["EUR"]; env.trust(EUR(100000), alice); env(pay(gw2, alice, EUR(3000))); env.close(); - env.require(balance(alice, gw2["EUR"](3000))); + env.require(balance(alice, EUR(3000))); // Alice creates AMM pool of EUR/USD. AMM amm(env, alice, EUR(1000), USD(2000), ter(tesSUCCESS)); @@ -1603,8 +1922,8 @@ class AMMClawback_test : public jtx::AMMTest ter(tesSUCCESS)); env.close(); - env.require(balance(alice, gw["USD"](1000))); - env.require(balance(alice, gw2["EUR"](2500))); + env.require(balance(alice, USD(1000))); + env.require(balance(alice, EUR(2500))); BEAST_EXPECT(amm.expectBalances( USD(1000), EUR(500), IOUAmount{7071067811865475, -13})); BEAST_EXPECT( @@ -1653,7 +1972,10 @@ class AMMClawback_test : public jtx::AMMTest amm.deposit(bob, USD(4000), EUR(1000)); BEAST_EXPECT( amm.expectBalances(USD(12000), EUR(3000), IOUAmount(6000))); - amm.deposit(carol, USD(2000), EUR(500)); + if (!features[fixAMMv1_3]) + amm.deposit(carol, USD(2000), EUR(500)); + else + amm.deposit(carol, USD(2000.25), EUR(500)); BEAST_EXPECT( amm.expectBalances(USD(14000), EUR(3500), IOUAmount(7000))); @@ -1675,7 +1997,12 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT(env.balance(alice, EUR) == EUR(8000)); BEAST_EXPECT(env.balance(bob, USD) == USD(5000)); BEAST_EXPECT(env.balance(bob, EUR) == EUR(8000)); - BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + else + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(5999'999999999999), -12)); // 250 EUR goes back to carol. BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750)); @@ -1697,7 +2024,12 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT(env.balance(bob, USD) == USD(5000)); // 250 EUR did not go back to bob because tfClawTwoAssets is set. BEAST_EXPECT(env.balance(bob, EUR) == EUR(8000)); - BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + else + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(5999'999999999999), -12)); BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750)); // gw clawback all USD from alice and set tfClawTwoAssets. @@ -1715,7 +2047,12 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT(env.balance(alice, EUR) == EUR(8000)); BEAST_EXPECT(env.balance(bob, USD) == USD(5000)); BEAST_EXPECT(env.balance(bob, EUR) == EUR(8000)); - BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + else + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(5999'999999999999), -12)); BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750)); } } @@ -1725,10 +2062,11 @@ class AMMClawback_test : public jtx::AMMTest { testcase("test single depoit and clawback"); using namespace jtx; + std::string logs; // Test AMMClawback for USD/XRP pool. Claw back USD, and XRP goes back // to the holder. - Env env(*this, features); + Env env(*this, features, std::make_unique(&logs)); Account gw{"gateway"}; Account alice{"alice"}; env.fund(XRP(1000000000), gw, alice); @@ -1744,7 +2082,7 @@ class AMMClawback_test : public jtx::AMMTest env.trust(USD(100000), alice); env(pay(gw, alice, USD(1000))); env.close(); - env.require(balance(alice, gw["USD"](1000))); + env.require(balance(alice, USD(1000))); // gw creates AMM pool of XRP/USD. AMM amm(env, gw, XRP(100), USD(400), ter(tesSUCCESS)); @@ -1763,30 +2101,368 @@ class AMMClawback_test : public jtx::AMMTest env(amm::ammClawback(gw, alice, USD, XRP, USD(400)), ter(tesSUCCESS)); env.close(); - BEAST_EXPECT(amm.expectBalances( - STAmount(USD, UINT64_C(5656854249492380), -13), - XRP(70.710678), - IOUAmount(200000))); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + STAmount(USD, UINT64_C(5656854249492380), -13), + XRP(70.710678), + IOUAmount(200000))); + else + BEAST_EXPECT(amm.expectBalances( + STAmount(USD, UINT64_C(565'685424949238), -12), + XRP(70.710679), + IOUAmount(200000))); BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0))); - BEAST_EXPECT(expectLedgerEntryRoot( - env, alice, aliceXrpBalance + XRP(29.289322))); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(expectLedgerEntryRoot( + env, alice, aliceXrpBalance + XRP(29.289322))); + else + BEAST_EXPECT(expectLedgerEntryRoot( + env, alice, aliceXrpBalance + XRP(29.289321))); + } + + void + testLastHolderLPTokenBalance(FeatureBitset features) + { + testcase( + "test last holder's lptoken balance not equal to AMM's lptoken " + "balance before clawback"); + using namespace jtx; + std::string logs; + + auto setupAccounts = + [&](Env& env, Account& gw, Account& alice, Account& bob) { + env.fund(XRP(100000), gw, alice, bob); + env.close(); + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + + auto const USD = gw["USD"]; + env.trust(USD(100000), alice); + env(pay(gw, alice, USD(50000))); + env.trust(USD(100000), bob); + env(pay(gw, bob, USD(40000))); + env.close(); + + return USD; + }; + + auto getLPTokenBalances = + [&](auto& env, + auto const& amm, + auto const& account) -> std::pair { + auto const lpToken = + getAccountLines( + env, account, amm.lptIssue())[jss::lines][0u][jss::balance] + .asString(); + auto const lpTokenBalance = + amm.ammRpcInfo()[jss::amm][jss::lp_token][jss::value] + .asString(); + return {lpToken, lpTokenBalance}; + }; + + // IOU/XRP pool. AMMClawback almost last holder's USD balance + { + Env env(*this, features, std::make_unique(&logs)); + Account gw{"gateway"}, alice{"alice"}, bob{"bob"}; + auto const USD = setupAccounts(env, gw, alice, bob); + + AMM amm(env, alice, XRP(2), USD(1)); + amm.deposit(alice, IOUAmount{1'876123487565916, -15}); + amm.deposit(bob, IOUAmount{1'000'000}); + amm.withdraw(alice, IOUAmount{1'876123487565916, -15}); + amm.withdrawAll(bob); + + auto [lpToken, lpTokenBalance] = + getLPTokenBalances(env, amm, alice); + BEAST_EXPECT( + lpToken == "1414.21356237366" && + lpTokenBalance == "1414.213562374"); + + auto res = + isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), alice); + BEAST_EXPECT(res && res.value()); + + if (!features[fixAMMClawbackRounding] || !features[fixAMMv1_3]) + { + env(amm::ammClawback(gw, alice, USD, XRP, USD(1)), + ter(tecAMM_BALANCE)); + BEAST_EXPECT(amm.ammExists()); + } + else + { + auto const lpBalance = IOUAmount{989, -12}; + env(amm::ammClawback(gw, alice, USD, XRP, USD(1))); + BEAST_EXPECT(amm.expectBalances( + STAmount(USD, UINT64_C(7000000000000000), -28), + XRPAmount(1), + lpBalance)); + BEAST_EXPECT(amm.expectLPTokens(alice, lpBalance)); + } + } + + // IOU/XRP pool. AMMClawback part of last holder's USD balance + { + Env env(*this, features, std::make_unique(&logs)); + Account gw{"gateway"}, alice{"alice"}, bob{"bob"}; + auto const USD = setupAccounts(env, gw, alice, bob); + + AMM amm(env, alice, XRP(2), USD(1)); + amm.deposit(alice, IOUAmount{1'876123487565916, -15}); + amm.deposit(bob, IOUAmount{1'000'000}); + amm.withdrawAll(bob); + + auto [lpToken, lpTokenBalance] = + getLPTokenBalances(env, amm, alice); + BEAST_EXPECT( + lpToken == "1416.08968586066" && + lpTokenBalance == "1416.089685861"); + + auto res = + isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), alice); + BEAST_EXPECT(res && res.value()); + + env(amm::ammClawback(gw, alice, USD, XRP, USD(0.5))); + + if (!features[fixAMMv1_3] && !features[fixAMMClawbackRounding]) + { + BEAST_EXPECT(amm.expectBalances( + STAmount(USD, UINT64_C(5013266196406), -13), + XRPAmount(1002653), + IOUAmount{708'9829046744236, -13})); + } + else if (!features[fixAMMClawbackRounding]) + { + BEAST_EXPECT(amm.expectBalances( + STAmount(USD, UINT64_C(5013266196407), -13), + XRPAmount(1002654), + IOUAmount{708'9829046744941, -13})); + } + else if (features[fixAMMv1_3] && features[fixAMMClawbackRounding]) + { + auto const lpBalance = IOUAmount{708'9829046743238, -13}; + BEAST_EXPECT(amm.expectBalances( + STAmount(USD, UINT64_C(5013266196406999), -16), + XRPAmount(1002655), + lpBalance)); + BEAST_EXPECT(amm.expectLPTokens(alice, lpBalance)); + } + } + + // IOU/XRP pool. AMMClawback all of last holder's USD balance + { + Env env(*this, features, std::make_unique(&logs)); + Account gw{"gateway"}, alice{"alice"}, bob{"bob"}; + auto const USD = setupAccounts(env, gw, alice, bob); + + AMM amm(env, alice, XRP(2), USD(1)); + amm.deposit(alice, IOUAmount{1'876123487565916, -15}); + amm.deposit(bob, IOUAmount{1'000'000}); + amm.withdraw(alice, IOUAmount{1'876123487565916, -15}); + amm.withdrawAll(bob); + + auto [lpToken, lpTokenBalance] = + getLPTokenBalances(env, amm, alice); + BEAST_EXPECT( + lpToken == "1414.21356237366" && + lpTokenBalance == "1414.213562374"); + + auto res = + isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), alice); + BEAST_EXPECT(res && res.value()); + + if (!features[fixAMMClawbackRounding] && !features[fixAMMv1_3]) + { + env(amm::ammClawback(gw, alice, USD, XRP, std::nullopt), + ter(tecAMM_BALANCE)); + } + else if (!features[fixAMMClawbackRounding]) + { + env(amm::ammClawback(gw, alice, USD, XRP, std::nullopt)); + BEAST_EXPECT(amm.expectBalances( + STAmount(USD, UINT64_C(2410000000000000), -28), + XRPAmount(1), + IOUAmount{34, -11})); + } + else if (features[fixAMMv1_3] && features[fixAMMClawbackRounding]) + { + env(amm::ammClawback(gw, alice, USD, XRP, std::nullopt)); + BEAST_EXPECT(!amm.ammExists()); + } + } + + // IOU/IOU pool, different issuers + { + Env env(*this, features, std::make_unique(&logs)); + Account gw{"gateway"}, alice{"alice"}, bob{"bob"}; + auto const USD = setupAccounts(env, gw, alice, bob); + + Account gw2{"gateway2"}; + env.fund(XRP(100000), gw2); + env.close(); + auto const EUR = gw2["EUR"]; + env.trust(EUR(100000), alice); + env(pay(gw2, alice, EUR(50000))); + env.trust(EUR(100000), bob); + env(pay(gw2, bob, EUR(50000))); + env.close(); + + AMM amm(env, alice, USD(2), EUR(1)); + amm.deposit(alice, IOUAmount{1'576123487565916, -15}); + amm.deposit(bob, IOUAmount{1'000}); + amm.withdraw(alice, IOUAmount{1'576123487565916, -15}); + amm.withdrawAll(bob); + + auto [lpToken, lpTokenBalance] = + getLPTokenBalances(env, amm, alice); + BEAST_EXPECT( + lpToken == "1.414213562374011" && + lpTokenBalance == "1.414213562374"); + + auto res = + isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), alice); + BEAST_EXPECT(res && res.value()); + + if (features[fixAMMv1_3] && features[fixAMMClawbackRounding]) + { + env(amm::ammClawback(gw, alice, USD, EUR, std::nullopt)); + BEAST_EXPECT(!amm.ammExists()); + } + else + { + env(amm::ammClawback(gw, alice, USD, EUR, std::nullopt), + ter(tecINTERNAL)); + BEAST_EXPECT(amm.ammExists()); + } + } + + // IOU/IOU pool, same issuer + { + Env env(*this, features, std::make_unique(&logs)); + Account gw{"gateway"}, alice{"alice"}, bob{"bob"}; + auto const USD = setupAccounts(env, gw, alice, bob); + + auto const EUR = gw["EUR"]; + env.trust(EUR(100000), alice); + env(pay(gw, alice, EUR(50000))); + env.trust(EUR(100000), bob); + env(pay(gw, bob, EUR(50000))); + env.close(); + + AMM amm(env, alice, USD(1), EUR(2)); + amm.deposit(alice, IOUAmount{1'076123487565916, -15}); + amm.deposit(bob, IOUAmount{1'000}); + amm.withdraw(alice, IOUAmount{1'076123487565916, -15}); + amm.withdrawAll(bob); + + auto [lpToken, lpTokenBalance] = + getLPTokenBalances(env, amm, alice); + BEAST_EXPECT( + lpToken == "1.414213562374011" && + lpTokenBalance == "1.414213562374"); + + auto res = + isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), alice); + BEAST_EXPECT(res && res.value()); + + if (features[fixAMMClawbackRounding]) + { + env(amm::ammClawback(gw, alice, USD, EUR, std::nullopt), + txflags(tfClawTwoAssets)); + BEAST_EXPECT(!amm.ammExists()); + } + else + { + env(amm::ammClawback(gw, alice, USD, EUR, std::nullopt), + txflags(tfClawTwoAssets), + ter(tecINTERNAL)); + BEAST_EXPECT(amm.ammExists()); + } + } + + // IOU/IOU pool, larger asset ratio + { + Env env(*this, features, std::make_unique(&logs)); + Account gw{"gateway"}, alice{"alice"}, bob{"bob"}; + auto const USD = setupAccounts(env, gw, alice, bob); + + auto const EUR = gw["EUR"]; + env.trust(EUR(1000000000), alice); + env(pay(gw, alice, EUR(500000000))); + env.trust(EUR(1000000000), bob); + env(pay(gw, bob, EUR(500000000))); + env.close(); + + AMM amm(env, alice, USD(1), EUR(2000000)); + amm.deposit(alice, IOUAmount{1'076123487565916, -12}); + amm.deposit(bob, IOUAmount{10000}); + amm.withdraw(alice, IOUAmount{1'076123487565916, -12}); + amm.withdrawAll(bob); + + auto [lpToken, lpTokenBalance] = + getLPTokenBalances(env, amm, alice); + + BEAST_EXPECT( + lpToken == "1414.213562373101" && + lpTokenBalance == "1414.2135623731"); + + auto res = + isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), alice); + BEAST_EXPECT(res && res.value()); + + if (!features[fixAMMClawbackRounding] && !features[fixAMMv1_3]) + { + env(amm::ammClawback(gw, alice, USD, EUR, USD(1))); + BEAST_EXPECT(amm.expectBalances( + STAmount(USD, UINT64_C(4), -15), + STAmount(EUR, UINT64_C(8), -9), + IOUAmount{6, -12})); + } + 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)); + BEAST_EXPECT(amm.ammExists()); + } + else if (features[fixAMMv1_3] && features[fixAMMClawbackRounding]) + { + env(amm::ammClawback(gw, alice, USD, EUR, USD(1)), + txflags(tfClawTwoAssets)); + auto const lpBalance = IOUAmount{5, -12}; + BEAST_EXPECT(amm.expectBalances( + STAmount(USD, UINT64_C(4), -15), + STAmount(EUR, UINT64_C(8), -9), + lpBalance)); + BEAST_EXPECT(amm.expectLPTokens(alice, lpBalance)); + } + } } void run() override { - FeatureBitset const all{jtx::supported_amendments()}; - testInvalidRequest(all); + FeatureBitset const all{ + jtx::testable_amendments() | fixAMMClawbackRounding}; + + testInvalidRequest(); testFeatureDisabled(all - featureAMMClawback); - testAMMClawbackSpecificAmount(all); - testAMMClawbackExceedBalance(all); - testAMMClawbackAll(all); - testAMMClawbackSameIssuerAssets(all); - testAMMClawbackSameCurrency(all); - testAMMClawbackIssuesEachOther(all); - testNotHoldingLptoken(all); - testAssetFrozen(all); - testSingleDepositAndClawback(all); + for (auto const& features : + {all - fixAMMv1_3 - fixAMMClawbackRounding, + all - fixAMMClawbackRounding, + all}) + { + testAMMClawbackSpecificAmount(features); + testAMMClawbackExceedBalance(features); + testAMMClawbackAll(features); + testAMMClawbackSameIssuerAssets(features); + testAMMClawbackSameCurrency(features); + testAMMClawbackIssuesEachOther(features); + testNotHoldingLptoken(features); + testAssetFrozen(features); + testSingleDepositAndClawback(features); + testLastHolderLPTokenBalance(features); + } } }; BEAST_DEFINE_TESTSUITE(AMMClawback, app, ripple); diff --git a/src/test/app/AMMExtended_test.cpp b/src/test/app/AMMExtended_test.cpp index d7caed9601..893e9e4f75 100644 --- a/src/test/app/AMMExtended_test.cpp +++ b/src/test/app/AMMExtended_test.cpp @@ -1183,9 +1183,7 @@ private: using namespace jtx; - // The problem was identified when featureOwnerPaysFee was enabled, - // so make sure that gets included. - Env env{*this, features | featureOwnerPaysFee}; + Env env{*this, features}; // The fee that's charged for transactions. auto const fee = env.current()->fees().base; @@ -1449,9 +1447,9 @@ private: testOffers() { using namespace jtx; - FeatureBitset const all{supported_amendments()}; + FeatureBitset const all{testable_amendments()}; testRmFundedOffer(all); - testRmFundedOffer(all - fixAMMv1_1); + testRmFundedOffer(all - fixAMMv1_1 - fixAMMv1_3); testEnforceNoRipple(all); testFillModes(all); testOfferCrossWithXRP(all); @@ -1465,7 +1463,7 @@ private: testOfferCreateThenCross(all); testSellFlagExceedLimit(all); testGatewayCrossCurrency(all); - testGatewayCrossCurrency(all - fixAMMv1_1); + testGatewayCrossCurrency(all - fixAMMv1_1 - fixAMMv1_3); testBridgedCross(all); testSellWithFillOrKill(all); testTransferRateOffer(all); @@ -1473,7 +1471,7 @@ private: testBadPathAssert(all); testSellFlagBasic(all); testDirectToDirectPath(all); - testDirectToDirectPath(all - fixAMMv1_1); + testDirectToDirectPath(all - fixAMMv1_1 - fixAMMv1_3); testRequireAuth(all); testMissingAuth(all); } @@ -2156,6 +2154,7 @@ private: OfferCrossing::no, std::nullopt, smax, + std::nullopt, flowJournal); }(); @@ -2216,271 +2215,6 @@ private: } } - void - testTransferRate(FeatureBitset features) - { - testcase("Transfer Rate"); - - using namespace jtx; - - { - // transfer fee on AMM - Env env(*this, features); - - fund(env, gw, {alice, bob, carol}, XRP(10'000), {USD(1'000)}); - env(rate(gw, 1.25)); - env.close(); - - AMM ammBob(env, bob, XRP(100), USD(150)); - // no transfer fee on create - BEAST_EXPECT(expectLine(env, bob, USD(1000 - 150))); - - env(pay(alice, carol, USD(50)), path(~USD), sendmax(XRP(50))); - env.close(); - - BEAST_EXPECT(expectLine(env, bob, USD(1'000 - 150))); - BEAST_EXPECT( - ammBob.expectBalances(XRP(150), USD(100), ammBob.tokens())); - BEAST_EXPECT(expectLedgerEntryRoot( - env, alice, xrpMinusFee(env, 10'000 - 50))); - BEAST_EXPECT(expectLine(env, carol, USD(1'050))); - } - - { - // Transfer fee AMM and offer - Env env(*this, features); - - fund( - env, - gw, - {alice, bob, carol}, - XRP(10'000), - {USD(1'000), EUR(1'000)}); - env(rate(gw, 1.25)); - env.close(); - - AMM ammBob(env, bob, XRP(100), USD(140)); - BEAST_EXPECT(expectLine(env, bob, USD(1'000 - 140))); - - env(offer(bob, USD(50), EUR(50))); - - // alice buys 40EUR with 40XRP - env(pay(alice, carol, EUR(40)), path(~USD, ~EUR), sendmax(XRP(40))); - - // 40XRP is swapped in for 40USD - BEAST_EXPECT( - ammBob.expectBalances(XRP(140), USD(100), ammBob.tokens())); - // 40USD buys 40EUR via bob's offer. 40EUR delivered to carol - // and bob pays 25% on 40EUR, 40EUR*0.25=10EUR - BEAST_EXPECT(expectLine(env, bob, EUR(1'000 - 40 - 40 * 0.25))); - // bob gets 40USD back from the offer - BEAST_EXPECT(expectLine(env, bob, USD(1'000 - 140 + 40))); - BEAST_EXPECT(expectLedgerEntryRoot( - env, alice, xrpMinusFee(env, 10'000 - 40))); - BEAST_EXPECT(expectLine(env, carol, EUR(1'040))); - BEAST_EXPECT(expectOffers(env, bob, 1, {{USD(10), EUR(10)}})); - } - - { - // Transfer fee two consecutive AMM - Env env(*this, features); - - fund( - env, - gw, - {alice, bob, carol}, - XRP(10'000), - {USD(1'000), EUR(1'000)}); - env(rate(gw, 1.25)); - env.close(); - - AMM ammBobXRP_USD(env, bob, XRP(100), USD(140)); - BEAST_EXPECT(expectLine(env, bob, USD(1'000 - 140))); - - AMM ammBobUSD_EUR(env, bob, USD(100), EUR(140)); - BEAST_EXPECT(expectLine(env, bob, EUR(1'000 - 140))); - BEAST_EXPECT(expectLine(env, bob, USD(1'000 - 140 - 100))); - - // alice buys 40EUR with 40XRP - env(pay(alice, carol, EUR(40)), path(~USD, ~EUR), sendmax(XRP(40))); - - // 40XRP is swapped in for 40USD - BEAST_EXPECT(ammBobXRP_USD.expectBalances( - XRP(140), USD(100), ammBobXRP_USD.tokens())); - // 40USD is swapped in for 40EUR - BEAST_EXPECT(ammBobUSD_EUR.expectBalances( - USD(140), EUR(100), ammBobUSD_EUR.tokens())); - // no other charges on bob - BEAST_EXPECT(expectLine(env, bob, USD(1'000 - 140 - 100))); - BEAST_EXPECT(expectLine(env, bob, EUR(1'000 - 140))); - BEAST_EXPECT(expectLedgerEntryRoot( - env, alice, xrpMinusFee(env, 10'000 - 40))); - BEAST_EXPECT(expectLine(env, carol, EUR(1'040))); - } - - { - // Payment via AMM with limit quality, deliver less - // than requested - Env env(*this, features); - - fund( - env, - gw, - {alice, bob, carol}, - XRP(1'000), - {USD(1'200), GBP(1'200)}); - env(rate(gw, 1.25)); - env.close(); - - AMM amm(env, bob, GBP(1'000), USD(1'100)); - - // requested quality limit is 90USD/110GBP = 0.8181 - // trade quality is 77.2727USD/94.4444GBP = 0.8181 - env(pay(alice, carol, USD(90)), - path(~USD), - sendmax(GBP(110)), - txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality)); - env.close(); - - if (!features[fixAMMv1_1]) - { - // alice buys 77.2727USD with 75.5555GBP and pays 25% tr fee - // on 75.5555GBP - // 1,200 - 75.55555*1.25 = 1200 - 94.4444 = 1105.55555GBP - BEAST_EXPECT(expectLine( - env, - alice, - STAmount{GBP, UINT64_C(1'105'555555555555), -12})); - // 75.5555GBP is swapped in for 77.7272USD - BEAST_EXPECT(amm.expectBalances( - STAmount{GBP, UINT64_C(1'075'555555555556), -12}, - STAmount{USD, UINT64_C(1'022'727272727272), -12}, - amm.tokens())); - } - else - { - // alice buys 77.2727USD with 75.5555GBP and pays 25% tr fee - // on 75.5555GBP - // 1,200 - 75.55555*1.25 = 1200 - 94.4444 = 1105.55555GBP - BEAST_EXPECT(expectLine( - env, - alice, - STAmount{GBP, UINT64_C(1'105'555555555554), -12})); - // 75.5555GBP is swapped in for 77.7272USD - BEAST_EXPECT(amm.expectBalances( - STAmount{GBP, UINT64_C(1'075'555555555557), -12}, - STAmount{USD, UINT64_C(1'022'727272727272), -12}, - amm.tokens())); - } - BEAST_EXPECT(expectLine( - env, carol, STAmount{USD, UINT64_C(1'277'272727272728), -12})); - } - - { - // AMM offer crossing - Env env(*this, features); - - fund(env, gw, {alice, bob}, XRP(1'000), {USD(1'200), EUR(1'200)}); - env(rate(gw, 1.25)); - env.close(); - - AMM amm(env, bob, USD(1'000), EUR(1'150)); - - env(offer(alice, EUR(100), USD(100))); - env.close(); - - if (!features[fixAMMv1_1]) - { - // 95.2380USD is swapped in for 100EUR - BEAST_EXPECT(amm.expectBalances( - STAmount{USD, UINT64_C(1'095'238095238095), -12}, - EUR(1'050), - amm.tokens())); - // alice pays 25% tr fee on 95.2380USD - // 1200-95.2380*1.25 = 1200 - 119.0477 = 1080.9523USD - BEAST_EXPECT(expectLine( - env, - alice, - STAmount{USD, UINT64_C(1'080'952380952381), -12}, - EUR(1'300))); - } - else - { - // 95.2380USD is swapped in for 100EUR - BEAST_EXPECT(amm.expectBalances( - STAmount{USD, UINT64_C(1'095'238095238096), -12}, - EUR(1'050), - amm.tokens())); - // alice pays 25% tr fee on 95.2380USD - // 1200-95.2380*1.25 = 1200 - 119.0477 = 1080.9523USD - BEAST_EXPECT(expectLine( - env, - alice, - STAmount{USD, UINT64_C(1'080'95238095238), -11}, - EUR(1'300))); - } - BEAST_EXPECT(expectOffers(env, alice, 0)); - } - - { - // First pass through a strand redeems, second pass issues, - // through an offer limiting step is not an endpoint - Env env(*this, features); - auto const USDA = alice["USD"]; - auto const USDB = bob["USD"]; - Account const dan("dan"); - - env.fund(XRP(10'000), bob, carol, dan, gw); - fund(env, {alice}, XRP(10'000)); - env(rate(gw, 1.25)); - env.trust(USD(2'000), alice, bob, carol, dan); - env.trust(EUR(2'000), carol, dan); - env.trust(USDA(1'000), bob); - env.trust(USDB(1'000), gw); - env(pay(gw, bob, USD(50))); - env(pay(gw, dan, EUR(1'050))); - env(pay(gw, dan, USD(1'000))); - AMM ammDan(env, dan, USD(1'000), EUR(1'050)); - - if (!features[fixAMMv1_1]) - { - // alice -> bob -> gw -> carol. $50 should have transfer fee; - // $10, no fee - env(pay(alice, carol, EUR(50)), - path(bob, gw, ~EUR), - sendmax(USDA(60)), - txflags(tfNoRippleDirect)); - BEAST_EXPECT(ammDan.expectBalances( - USD(1'050), EUR(1'000), ammDan.tokens())); - BEAST_EXPECT(expectLine(env, dan, USD(0))); - BEAST_EXPECT(expectLine(env, dan, EUR(0))); - BEAST_EXPECT(expectLine(env, bob, USD(-10))); - BEAST_EXPECT(expectLine(env, bob, USDA(60))); - BEAST_EXPECT(expectLine(env, carol, EUR(50))); - } - else - { - // alice -> bob -> gw -> carol. $50 should have transfer fee; - // $10, no fee - env(pay(alice, carol, EUR(50)), - path(bob, gw, ~EUR), - sendmax(USDA(60.1)), - txflags(tfNoRippleDirect)); - BEAST_EXPECT(ammDan.expectBalances( - STAmount{USD, UINT64_C(1'050'000000000001), -12}, - EUR(1'000), - ammDan.tokens())); - BEAST_EXPECT(expectLine(env, dan, USD(0))); - BEAST_EXPECT(expectLine(env, dan, EUR(0))); - BEAST_EXPECT(expectLine( - env, bob, STAmount{USD, INT64_C(-10'000000000001), -12})); - BEAST_EXPECT(expectLine( - env, bob, STAmount{USDA, UINT64_C(60'000000000001), -12})); - BEAST_EXPECT(expectLine(env, carol, EUR(50))); - } - } - } - void testTransferRateNoOwnerFee(FeatureBitset features) { @@ -3089,8 +2823,8 @@ private: for (auto const withFix : {true, false}) { auto const feats = withFix - ? supported_amendments() - : supported_amendments() - FeatureBitset{fix1781}; + ? testable_amendments() + : testable_amendments() - FeatureBitset{fix1781}; // Payment path starting with XRP Env env(*this, feats); @@ -4055,16 +3789,12 @@ private: testFlow() { using namespace jtx; - FeatureBitset const all{supported_amendments()}; - FeatureBitset const ownerPaysFee{featureOwnerPaysFee}; + FeatureBitset const all{testable_amendments()}; testFalseDry(all); testBookStep(all); - testBookStep(all | ownerPaysFee); - testTransferRate(all | ownerPaysFee); - testTransferRate((all - fixAMMv1_1) | ownerPaysFee); testTransferRateNoOwnerFee(all); - testTransferRateNoOwnerFee(all - fixAMMv1_1); + testTransferRateNoOwnerFee(all - fixAMMv1_1 - fixAMMv1_3); testLimitQuality(); testXRPPathLoop(); } @@ -4073,24 +3803,24 @@ private: testCrossingLimits() { using namespace jtx; - FeatureBitset const all{supported_amendments()}; + FeatureBitset const all{testable_amendments()}; testStepLimit(all); - testStepLimit(all - fixAMMv1_1); + testStepLimit(all - fixAMMv1_1 - fixAMMv1_3); } void testDeliverMin() { using namespace jtx; - FeatureBitset const all{supported_amendments()}; + FeatureBitset const all{testable_amendments()}; test_convert_all_of_an_asset(all); - test_convert_all_of_an_asset(all - fixAMMv1_1); + test_convert_all_of_an_asset(all - fixAMMv1_1 - fixAMMv1_3); } void testDepositAuth() { - auto const supported{jtx::supported_amendments()}; + auto const supported{jtx::testable_amendments()}; testPayment(supported - featureDepositPreauth); testPayment(supported); testPayIOU(); @@ -4100,7 +3830,7 @@ private: testFreeze() { using namespace test::jtx; - auto const sa = supported_amendments(); + auto const sa = testable_amendments(); testRippleState(sa); testGlobalFreeze(sa); testOffersWhenFrozen(sa); @@ -4110,7 +3840,7 @@ private: testMultisign() { using namespace jtx; - auto const all = supported_amendments(); + auto const all = testable_amendments(); testTxMultisign( all - featureMultiSignReserve - featureExpandedSignerList); @@ -4122,7 +3852,7 @@ private: testPayStrand() { using namespace jtx; - auto const all = supported_amendments(); + auto const all = testable_amendments(); testToStrand(all); testRIPD1373(all); diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index 9d926fdcc1..382c24dec7 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -27,6 +27,7 @@ #include #include +#include #include #include @@ -65,7 +66,7 @@ private: {}, 0, {}, - {supported_amendments() | featureSingleAssetVault}); + {testable_amendments() | featureSingleAssetVault}); // XRP to IOU, without featureSingleAssetVault testAMM( @@ -76,7 +77,7 @@ private: {}, 0, {}, - {supported_amendments() - featureSingleAssetVault}); + {testable_amendments() - featureSingleAssetVault}); // IOU to IOU testAMM( @@ -836,21 +837,6 @@ private: std::nullopt, ter(tecAMM_FAILED)); - // Tiny deposit - ammAlice.deposit( - carol, - IOUAmount{1, -4}, - std::nullopt, - std::nullopt, - ter(temBAD_AMOUNT)); - ammAlice.deposit( - carol, - STAmount{USD, 1, -12}, - std::nullopt, - std::nullopt, - std::nullopt, - ter(tecAMM_INVALID_TOKENS)); - // Deposit non-empty AMM ammAlice.deposit( carol, @@ -861,6 +847,34 @@ private: ter(tecAMM_NOT_EMPTY)); }); + // Tiny deposit + testAMM( + [&](AMM& ammAlice, Env& env) { + auto const enabledv1_3 = + env.current()->rules().enabled(fixAMMv1_3); + auto const err = + !enabledv1_3 ? ter(temBAD_AMOUNT) : ter(tesSUCCESS); + // Pre-amendment XRP deposit side is rounded to 0 + // and deposit fails. + // Post-amendment XRP deposit side is rounded to 1 + // and deposit succeeds. + ammAlice.deposit( + carol, IOUAmount{1, -4}, std::nullopt, std::nullopt, err); + // Pre/post-amendment LPTokens is rounded to 0 and deposit + // fails with tecAMM_INVALID_TOKENS. + ammAlice.deposit( + carol, + STAmount{USD, 1, -12}, + std::nullopt, + std::nullopt, + std::nullopt, + ter(tecAMM_INVALID_TOKENS)); + }, + std::nullopt, + 0, + std::nullopt, + {features, features - fixAMMv1_3}); + // Invalid AMM testAMM([&](AMM& ammAlice, Env& env) { ammAlice.withdrawAll(alice); @@ -1316,6 +1330,59 @@ private: std::nullopt, ter(tecAMM_FAILED)); }); + + // Equal deposit, tokens rounded to 0 + testAMM([&](AMM& amm, Env& env) { + amm.deposit( + DepositArg{ + .tokens = IOUAmount{1, -12}, + .err = ter(tecAMM_INVALID_TOKENS)}); + }); + + // Equal deposit limit, tokens rounded to 0 + testAMM( + [&](AMM& amm, Env& env) { + amm.deposit( + DepositArg{ + .asset1In = STAmount{USD, 1, -15}, + .asset2In = XRPAmount{1}, + .err = ter(tecAMM_INVALID_TOKENS)}); + }, + {.pool = {{USD(1'000'000), XRP(1'000'000)}}, + .features = {features - fixAMMv1_3}}); + testAMM([&](AMM& amm, Env& env) { + amm.deposit( + DepositArg{ + .asset1In = STAmount{USD, 1, -15}, + .asset2In = XRPAmount{1}, + .err = ter(tecAMM_INVALID_TOKENS)}); + }); + + // Single deposit by asset, tokens rounded to 0 + testAMM([&](AMM& amm, Env& env) { + amm.deposit( + DepositArg{ + .asset1In = STAmount{USD, 1, -15}, + .err = ter(tecAMM_INVALID_TOKENS)}); + }); + + // Single deposit by tokens, tokens rounded to 0 + testAMM([&](AMM& amm, Env& env) { + amm.deposit( + DepositArg{ + .tokens = IOUAmount{1, -10}, + .asset1In = STAmount{USD, 1, -15}, + .err = ter(tecAMM_INVALID_TOKENS)}); + }); + + // Single deposit with eprice, tokens rounded to 0 + testAMM([&](AMM& amm, Env& env) { + amm.deposit( + DepositArg{ + .asset1In = STAmount{USD, 1, -15}, + .maxEP = STAmount{USD, 1, -1}, + .err = ter(tecAMM_INVALID_TOKENS)}); + }); } void @@ -1324,6 +1391,7 @@ private: testcase("Deposit"); using namespace jtx; + auto const all = testable_amendments(); // Equal deposit: 1000000 tokens, 10% of the current pool testAMM([&](AMM& ammAlice, Env& env) { @@ -1529,8 +1597,9 @@ private: }); // Issuer create/deposit + for (auto const& feat : {all, all - fixAMMv1_3}) { - Env env(*this); + Env env(*this, feat); env.fund(XRP(30000), gw); AMM ammGw(env, gw, XRP(10'000), USD(10'000)); BEAST_EXPECT( @@ -1624,6 +1693,7 @@ private: testcase("Invalid Withdraw"); using namespace jtx; + auto const all = testable_amendments(); testAMM( [&](AMM& ammAlice, Env& env) { @@ -1918,16 +1988,6 @@ private: ammAlice.withdraw( carol, 10'000, std::nullopt, std::nullopt, ter(tecAMM_BALANCE)); - // Withdraw entire one side of the pool. - // Equal withdraw but due to XRP precision limit, - // this results in full withdraw of XRP pool only, - // while leaving a tiny amount in USD pool. - ammAlice.withdraw( - alice, - IOUAmount{9'999'999'9999, -4}, - std::nullopt, - std::nullopt, - ter(tecAMM_BALANCE)); // Withdrawing from one side. // XRP by tokens ammAlice.withdraw( @@ -1959,6 +2019,57 @@ private: ter(tecAMM_BALANCE)); }); + testAMM( + [&](AMM& ammAlice, Env& env) { + // Withdraw entire one side of the pool. + // Pre-amendment: + // Equal withdraw but due to XRP rounding + // this results in full withdraw of XRP pool only, + // while leaving a tiny amount in USD pool. + // Post-amendment: + // Most of the pool is withdrawn with remaining tiny amounts + auto err = env.enabled(fixAMMv1_3) ? ter(tesSUCCESS) + : ter(tecAMM_BALANCE); + ammAlice.withdraw( + alice, + IOUAmount{9'999'999'9999, -4}, + std::nullopt, + std::nullopt, + err); + if (env.enabled(fixAMMv1_3)) + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(1), STAmount{USD, 1, -7}, IOUAmount{1, -4})); + }, + std::nullopt, + 0, + std::nullopt, + {all, all - fixAMMv1_3}); + + testAMM( + [&](AMM& ammAlice, Env& env) { + // Similar to above with even smaller remaining amount + // is it ok that the pool is unbalanced? + // Withdraw entire one side of the pool. + // Equal withdraw but due to XRP precision limit, + // this results in full withdraw of XRP pool only, + // while leaving a tiny amount in USD pool. + auto err = env.enabled(fixAMMv1_3) ? ter(tesSUCCESS) + : ter(tecAMM_BALANCE); + ammAlice.withdraw( + alice, + IOUAmount{9'999'999'999999999, -9}, + std::nullopt, + std::nullopt, + err); + if (env.enabled(fixAMMv1_3)) + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(1), STAmount{USD, 1, -11}, IOUAmount{1, -8})); + }, + std::nullopt, + 0, + std::nullopt, + {all, all - fixAMMv1_3}); + // Invalid AMM testAMM([&](AMM& ammAlice, Env& env) { ammAlice.withdrawAll(alice); @@ -2022,15 +2133,19 @@ private: // Withdraw with EPrice limit. Fails to withdraw, calculated tokens // to withdraw are 0. - testAMM([&](AMM& ammAlice, Env&) { - ammAlice.deposit(carol, 1'000'000); - ammAlice.withdraw( - carol, - USD(100), - std::nullopt, - IOUAmount{500, 0}, - ter(tecAMM_FAILED)); - }); + testAMM( + [&](AMM& ammAlice, Env& env) { + ammAlice.deposit(carol, 1'000'000); + auto const err = env.enabled(fixAMMv1_3) + ? ter(tecAMM_INVALID_TOKENS) + : ter(tecAMM_FAILED); + ammAlice.withdraw( + carol, USD(100), std::nullopt, IOUAmount{500, 0}, err); + }, + std::nullopt, + 0, + std::nullopt, + {all, all - fixAMMv1_3}); // Withdraw with EPrice limit. Fails to withdraw, calculated tokens // to withdraw are greater than the LP shares. @@ -2095,14 +2210,19 @@ private: // Withdraw close to one side of the pool. Account's LP tokens // are rounded to all LP tokens. - testAMM([&](AMM& ammAlice, Env&) { - ammAlice.withdraw( - alice, - STAmount{USD, UINT64_C(9'999'999999999999), -12}, - std::nullopt, - std::nullopt, - ter(tecAMM_BALANCE)); - }); + testAMM( + [&](AMM& ammAlice, Env& env) { + auto const err = env.enabled(fixAMMv1_3) + ? ter(tecINVARIANT_FAILED) + : ter(tecAMM_BALANCE); + ammAlice.withdraw( + alice, + STAmount{USD, UINT64_C(9'999'999999999999), -12}, + std::nullopt, + std::nullopt, + err); + }, + {.features = {all, all - fixAMMv1_3}, .noLog = true}); // Tiny withdraw testAMM([&](AMM& ammAlice, Env&) { @@ -2133,6 +2253,20 @@ private: XRPAmount{1}, std::nullopt, ter(tecAMM_INVALID_TOKENS)); + ammAlice.withdraw( + WithdrawArg{ + .tokens = IOUAmount{1, -10}, + .err = ter(tecAMM_INVALID_TOKENS)}); + ammAlice.withdraw( + WithdrawArg{ + .asset1Out = STAmount{USD, 1, -15}, + .asset2Out = XRPAmount{1}, + .err = ter(tecAMM_INVALID_TOKENS)}); + ammAlice.withdraw( + WithdrawArg{ + .tokens = IOUAmount{1, -10}, + .asset1Out = STAmount{USD, 1, -15}, + .err = ter(tecAMM_INVALID_TOKENS)}); }); } @@ -2142,6 +2276,7 @@ private: testcase("Withdraw"); using namespace jtx; + auto const all = testable_amendments(); // Equal withdrawal by Carol: 1000000 of tokens, 10% of the current // pool @@ -2196,11 +2331,24 @@ private: }); // Single withdrawal by amount XRP1000 - testAMM([&](AMM& ammAlice, Env&) { - ammAlice.withdraw(alice, XRP(1'000)); - BEAST_EXPECT(ammAlice.expectBalances( - XRP(9'000), USD(10'000), IOUAmount{9'486'832'98050514, -8})); - }); + testAMM( + [&](AMM& ammAlice, Env& env) { + ammAlice.withdraw(alice, XRP(1'000)); + if (!env.enabled(fixAMMv1_3)) + BEAST_EXPECT(ammAlice.expectBalances( + XRP(9'000), + USD(10'000), + IOUAmount{9'486'832'98050514, -8})); + else + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{9'000'000'001}, + USD(10'000), + IOUAmount{9'486'832'98050514, -8})); + }, + std::nullopt, + 0, + std::nullopt, + {all, all - fixAMMv1_3}); // Single withdrawal by tokens 10000. testAMM([&](AMM& ammAlice, Env&) { @@ -2251,20 +2399,31 @@ private: }); // Single deposit/withdraw by the same account - testAMM([&](AMM& ammAlice, Env&) { - // Since a smaller amount might be deposited due to - // the lp tokens adjustment, withdrawing by tokens - // is generally preferred to withdrawing by amount. - auto lpTokens = ammAlice.deposit(carol, USD(1'000)); - ammAlice.withdraw(carol, lpTokens, USD(0)); - lpTokens = ammAlice.deposit(carol, STAmount(USD, 1, -6)); - ammAlice.withdraw(carol, lpTokens, USD(0)); - lpTokens = ammAlice.deposit(carol, XRPAmount(1)); - ammAlice.withdraw(carol, lpTokens, XRPAmount(0)); - BEAST_EXPECT(ammAlice.expectBalances( - XRP(10'000), USD(10'000), ammAlice.tokens())); - BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0})); - }); + testAMM( + [&](AMM& ammAlice, Env& env) { + // Since a smaller amount might be deposited due to + // the lp tokens adjustment, withdrawing by tokens + // is generally preferred to withdrawing by amount. + auto lpTokens = ammAlice.deposit(carol, USD(1'000)); + ammAlice.withdraw(carol, lpTokens, USD(0)); + lpTokens = ammAlice.deposit(carol, STAmount(USD, 1, -6)); + ammAlice.withdraw(carol, lpTokens, USD(0)); + lpTokens = ammAlice.deposit(carol, XRPAmount(1)); + ammAlice.withdraw(carol, lpTokens, XRPAmount(0)); + if (!env.enabled(fixAMMv1_3)) + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10'000), USD(10'000), ammAlice.tokens())); + else + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(10'000'000'001), + USD(10'000), + ammAlice.tokens())); + BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0})); + }, + std::nullopt, + 0, + std::nullopt, + {all, all - fixAMMv1_3}); // Single deposit by different accounts and then withdraw // in reverse. @@ -2307,36 +2466,34 @@ private: IOUAmount{10'000'000, 0})); }); - auto const all = supported_amendments(); // Withdraw with EPrice limit. testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.deposit(carol, 1'000'000); ammAlice.withdraw( carol, USD(100), std::nullopt, IOUAmount{520, 0}); - if (!env.current()->rules().enabled(fixAMMv1_1)) - BEAST_EXPECT( - ammAlice.expectBalances( - XRPAmount(11'000'000'000), - STAmount{USD, UINT64_C(9'372'781065088757), -12}, - IOUAmount{10'153'846'15384616, -8}) && - ammAlice.expectLPTokens( - carol, IOUAmount{153'846'15384616, -8})); - else - BEAST_EXPECT( - ammAlice.expectBalances( - XRPAmount(11'000'000'000), - STAmount{USD, UINT64_C(9'372'781065088769), -12}, - IOUAmount{10'153'846'15384616, -8}) && - ammAlice.expectLPTokens( - carol, IOUAmount{153'846'15384616, -8})); + BEAST_EXPECT(ammAlice.expectLPTokens( + carol, IOUAmount{153'846'15384616, -8})); + if (!env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3)) + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(11'000'000'000), + STAmount{USD, UINT64_C(9'372'781065088757), -12}, + IOUAmount{10'153'846'15384616, -8})); + else if (env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3)) + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(11'000'000'000), + STAmount{USD, UINT64_C(9'372'781065088769), -12}, + IOUAmount{10'153'846'15384616, -8})); + else if (env.enabled(fixAMMv1_3)) + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(11'000'000'000), + STAmount{USD, UINT64_C(9'372'78106508877), -11}, + IOUAmount{10'153'846'15384616, -8})); ammAlice.withdrawAll(carol); BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0})); }, - std::nullopt, - 0, - std::nullopt, - {all, all - fixAMMv1_1}); + {.features = {all, all - fixAMMv1_3, all - fixAMMv1_1 - fixAMMv1_3}, + .noLog = true}); // Withdraw with EPrice limit. AssetOut is 0. testAMM( @@ -2344,27 +2501,28 @@ private: ammAlice.deposit(carol, 1'000'000); ammAlice.withdraw( carol, USD(0), std::nullopt, IOUAmount{520, 0}); - if (!env.current()->rules().enabled(fixAMMv1_1)) - BEAST_EXPECT( - ammAlice.expectBalances( - XRPAmount(11'000'000'000), - STAmount{USD, UINT64_C(9'372'781065088757), -12}, - IOUAmount{10'153'846'15384616, -8}) && - ammAlice.expectLPTokens( - carol, IOUAmount{153'846'15384616, -8})); - else - BEAST_EXPECT( - ammAlice.expectBalances( - XRPAmount(11'000'000'000), - STAmount{USD, UINT64_C(9'372'781065088769), -12}, - IOUAmount{10'153'846'15384616, -8}) && - ammAlice.expectLPTokens( - carol, IOUAmount{153'846'15384616, -8})); + BEAST_EXPECT(ammAlice.expectLPTokens( + carol, IOUAmount{153'846'15384616, -8})); + if (!env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3)) + BEAST_EXPECT(ammAlice.expectBalances( + XRP(11'000), + STAmount{USD, UINT64_C(9'372'781065088757), -12}, + IOUAmount{10'153'846'15384616, -8})); + else if (env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3)) + BEAST_EXPECT(ammAlice.expectBalances( + XRP(11'000), + STAmount{USD, UINT64_C(9'372'781065088769), -12}, + IOUAmount{10'153'846'15384616, -8})); + else if (env.enabled(fixAMMv1_3)) + BEAST_EXPECT(ammAlice.expectBalances( + XRP(11'000), + STAmount{USD, UINT64_C(9'372'78106508877), -11}, + IOUAmount{10'153'846'15384616, -8})); }, std::nullopt, 0, std::nullopt, - {all, all - fixAMMv1_1}); + {all, all - fixAMMv1_3, all - fixAMMv1_1 - fixAMMv1_3}); // IOU to IOU + transfer fee { @@ -2403,14 +2561,25 @@ private: STAmount{USD, UINT64_C(9'999'999999), -6}, IOUAmount{9'999'999'999, -3})); }); - testAMM([&](AMM& ammAlice, Env&) { - // Single XRP pool - ammAlice.withdraw(alice, std::nullopt, XRPAmount{1}); - BEAST_EXPECT(ammAlice.expectBalances( - XRPAmount{9'999'999'999}, - USD(10'000), - IOUAmount{9'999'999'9995, -4})); - }); + testAMM( + [&](AMM& ammAlice, Env& env) { + // Single XRP pool + ammAlice.withdraw(alice, std::nullopt, XRPAmount{1}); + if (!env.enabled(fixAMMv1_3)) + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{9'999'999'999}, + USD(10'000), + IOUAmount{9'999'999'9995, -4})); + else + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10'000), + USD(10'000), + IOUAmount{9'999'999'9995, -4})); + }, + std::nullopt, + 0, + std::nullopt, + {all, all - fixAMMv1_3}); testAMM([&](AMM& ammAlice, Env&) { // Single USD pool ammAlice.withdraw(alice, std::nullopt, STAmount{USD, 1, -10}); @@ -2528,6 +2697,7 @@ private: { testcase("Fee Vote"); using namespace jtx; + auto const all = testable_amendments(); // One vote sets fee to 1%. testAMM([&](AMM& ammAlice, Env& env) { @@ -2545,6 +2715,12 @@ private: std::uint32_t tokens = 10'000'000, std::vector* accounts = nullptr) { Account a(std::to_string(i)); + // post-amendment the amount to deposit is slightly higher + // in order to ensure AMM invariant sqrt(asset1 * asset2) >= tokens + // fund just one USD higher in this case, which is enough for + // deposit to succeed + if (env.enabled(fixAMMv1_3)) + ++fundUSD; fund(env, gw, {a}, {USD(fundUSD)}, Fund::Acct); ammAlice.deposit(a, tokens); ammAlice.vote(a, 50 * (i + 1)); @@ -2553,11 +2729,16 @@ private: }; // Eight votes fill all voting slots, set fee 0.175%. - testAMM([&](AMM& ammAlice, Env& env) { - for (int i = 0; i < 7; ++i) - vote(ammAlice, env, i, 10'000); - BEAST_EXPECT(ammAlice.expectTradingFee(175)); - }); + testAMM( + [&](AMM& ammAlice, Env& env) { + for (int i = 0; i < 7; ++i) + vote(ammAlice, env, i, 10'000); + BEAST_EXPECT(ammAlice.expectTradingFee(175)); + }, + std::nullopt, + 0, + std::nullopt, + {all}); // Eight votes fill all voting slots, set fee 0.175%. // New vote, same account, sets fee 0.225% @@ -2951,8 +3132,14 @@ private: fund(env, gw, {bob}, {USD(10'000)}, Fund::Acct); ammAlice.deposit(bob, 1'000'000); - BEAST_EXPECT(ammAlice.expectBalances( - XRP(12'000), USD(12'000), IOUAmount{12'000'000, 0})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(ammAlice.expectBalances( + XRP(12'000), USD(12'000), IOUAmount{12'000'000, 0})); + else + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{12'000'000'001}, + USD(12'000), + IOUAmount{12'000'000, 0})); // Initial state. Pay bidMin. env(ammAlice.bid({.account = carol, .bidMin = 110})).close(); @@ -2984,8 +3171,16 @@ private: BEAST_EXPECT(ammAlice.expectAuctionSlot( 0, std::nullopt, IOUAmount{110})); // ~321.09 tokens burnt on bidding fees. - BEAST_EXPECT(ammAlice.expectBalances( - XRP(12'000), USD(12'000), IOUAmount{11'999'678'91, -2})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(ammAlice.expectBalances( + XRP(12'000), + USD(12'000), + IOUAmount{11'999'678'91, -2})); + else + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{12'000'000'001}, + USD(12'000), + IOUAmount{11'999'678'91, -2})); }, std::nullopt, 0, @@ -3014,8 +3209,12 @@ private: auto const slotPrice = IOUAmount{5'200}; ammTokens -= slotPrice; BEAST_EXPECT(ammAlice.expectAuctionSlot(100, 0, slotPrice)); - BEAST_EXPECT(ammAlice.expectBalances( - XRP(13'000), USD(13'000), ammTokens)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(ammAlice.expectBalances( + XRP(13'000), USD(13'000), ammTokens)); + else + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{13'000'000'003}, USD(13'000), ammTokens)); // Discounted trade for (int i = 0; i < 10; ++i) { @@ -3056,10 +3255,16 @@ private: env.balance(ed, USD) == STAmount(USD, UINT64_C(18'999'0057261184), -10)); // USD pool is slightly higher because of the fees. - BEAST_EXPECT(ammAlice.expectBalances( - XRP(13'000), - STAmount(USD, UINT64_C(13'002'98282151422), -11), - ammTokens)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(ammAlice.expectBalances( + XRP(13'000), + STAmount(USD, UINT64_C(13'002'98282151422), -11), + ammTokens)); + else + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{13'000'000'003}, + STAmount(USD, UINT64_C(13'002'98282151422), -11), + ammTokens)); } ammTokens = ammAlice.getLPTokensBalance(); // Trade with the fee @@ -3101,31 +3306,54 @@ private: } else { - BEAST_EXPECT( - env.balance(dan, USD) == - STAmount(USD, UINT64_C(19'490'05672274399), -11)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT( + env.balance(dan, USD) == + STAmount(USD, UINT64_C(19'490'05672274399), -11)); + else + BEAST_EXPECT( + env.balance(dan, USD) == + STAmount(USD, UINT64_C(19'490'05672274398), -11)); // USD pool gains more in dan's fees. - BEAST_EXPECT(ammAlice.expectBalances( - XRP(13'000), - STAmount{USD, UINT64_C(13'012'92609877023), -11}, - ammTokens)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(ammAlice.expectBalances( + XRP(13'000), + STAmount{USD, UINT64_C(13'012'92609877023), -11}, + ammTokens)); + else + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{13'000'000'003}, + STAmount{USD, UINT64_C(13'012'92609877024), -11}, + ammTokens)); // Discounted fee payment ammAlice.deposit(carol, USD(100)); ammTokens = ammAlice.getLPTokensBalance(); - BEAST_EXPECT(ammAlice.expectBalances( - XRP(13'000), - STAmount{USD, UINT64_C(13'112'92609877023), -11}, - ammTokens)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(ammAlice.expectBalances( + XRP(13'000), + STAmount{USD, UINT64_C(13'112'92609877023), -11}, + ammTokens)); + else + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{13'000'000'003}, + STAmount{USD, UINT64_C(13'112'92609877024), -11}, + ammTokens)); env(pay(carol, bob, USD(100)), path(~USD), sendmax(XRP(110))); env.close(); // carol pays 100000 drops in fees // 99900668XRP swapped in for 100USD - BEAST_EXPECT(ammAlice.expectBalances( - XRPAmount{13'100'000'668}, - STAmount{USD, UINT64_C(13'012'92609877023), -11}, - ammTokens)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{13'100'000'668}, + STAmount{USD, UINT64_C(13'012'92609877023), -11}, + ammTokens)); + else + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{13'100'000'671}, + STAmount{USD, UINT64_C(13'012'92609877024), -11}, + ammTokens)); } // Payment with the trading fee env(pay(alice, carol, XRP(100)), path(~XRP), sendmax(USD(110))); @@ -3133,20 +3361,27 @@ private: // alice pays ~1.011USD in fees, which is ~10 times more // than carol's fee // 100.099431529USD swapped in for 100XRP - if (!features[fixAMMv1_1]) + if (!features[fixAMMv1_1] && !features[fixAMMv1_3]) { BEAST_EXPECT(ammAlice.expectBalances( XRPAmount{13'000'000'668}, STAmount{USD, UINT64_C(13'114'03663047264), -11}, ammTokens)); } - else + else if (features[fixAMMv1_1] && !features[fixAMMv1_3]) { BEAST_EXPECT(ammAlice.expectBalances( XRPAmount{13'000'000'668}, STAmount{USD, UINT64_C(13'114'03663047269), -11}, ammTokens)); } + else + { + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{13'000'000'671}, + STAmount{USD, UINT64_C(13'114'03663044937), -11}, + ammTokens)); + } // Auction slot expired, no discounted fee env.close(seconds(TOTAL_TIME_SLOT_SECS + 1)); // clock is parent's based @@ -3155,7 +3390,7 @@ private: BEAST_EXPECT( env.balance(carol, USD) == STAmount(USD, UINT64_C(29'399'00572620545), -11)); - else + else if (!features[fixAMMv1_3]) BEAST_EXPECT( env.balance(carol, USD) == STAmount(USD, UINT64_C(29'399'00572620544), -11)); @@ -3167,7 +3402,7 @@ private: } // carol pays ~9.94USD in fees, which is ~10 times more in // trading fees vs discounted fee. - if (!features[fixAMMv1_1]) + if (!features[fixAMMv1_1] && !features[fixAMMv1_3]) { BEAST_EXPECT( env.balance(carol, USD) == @@ -3177,7 +3412,7 @@ private: STAmount{USD, UINT64_C(13'123'98038490681), -11}, ammTokens)); } - else + else if (features[fixAMMv1_1] && !features[fixAMMv1_3]) { BEAST_EXPECT( env.balance(carol, USD) == @@ -3187,25 +3422,42 @@ private: STAmount{USD, UINT64_C(13'123'98038490689), -11}, ammTokens)); } + else + { + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(29'389'06197177129), -11)); + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{13'000'000'671}, + STAmount{USD, UINT64_C(13'123'98038488352), -11}, + ammTokens)); + } env(pay(carol, bob, USD(100)), path(~USD), sendmax(XRP(110))); env.close(); // carol pays ~1.008XRP in trading fee, which is // ~10 times more than the discounted fee. // 99.815876XRP is swapped in for 100USD - if (!features[fixAMMv1_1]) + if (!features[fixAMMv1_1] && !features[fixAMMv1_3]) { BEAST_EXPECT(ammAlice.expectBalances( XRPAmount(13'100'824'790), STAmount{USD, UINT64_C(13'023'98038490681), -11}, ammTokens)); } - else + else if (features[fixAMMv1_1] && !features[fixAMMv1_3]) { BEAST_EXPECT(ammAlice.expectBalances( XRPAmount(13'100'824'790), STAmount{USD, UINT64_C(13'023'98038490689), -11}, ammTokens)); } + else + { + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(13'100'824'793), + STAmount{USD, UINT64_C(13'023'98038488352), -11}, + ammTokens)); + } }, std::nullopt, 1'000, @@ -3408,10 +3660,10 @@ private: // Can't pay into AMM with escrow. testAMM([&](AMM& ammAlice, Env& env) { auto const baseFee = env.current()->fees().base; - env(escrow(carol, ammAlice.ammAccount(), XRP(1)), - condition(cb1), - finish_time(env.now() + 1s), - cancel_time(env.now() + 2s), + env(escrow::create(carol, ammAlice.ammAccount(), XRP(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s), fee(baseFee * 150), ter(tecNO_PERMISSION)); }); @@ -4506,7 +4758,7 @@ private: testAMM([&](AMM& ammAlice, Env& env) { auto const baseFee = env.current()->fees().base.drops(); auto const token1 = ammAlice.lptIssue(); - auto priceXRP = withdrawByTokens( + auto priceXRP = ammAssetOut( STAmount{XRPAmount{10'000'000'000}}, STAmount{token1, 10'000'000}, STAmount{token1, 5'000'000}, @@ -4533,7 +4785,7 @@ private: BEAST_EXPECT( accountBalance(env, carol) == std::to_string(22500000000 - 4 * baseFee)); - priceXRP = withdrawByTokens( + priceXRP = ammAssetOut( STAmount{XRPAmount{10'000'000'000}}, STAmount{token1, 9'999'900}, STAmount{token1, 4'999'900}, @@ -4612,7 +4864,7 @@ private: { testcase("Amendment"); using namespace jtx; - FeatureBitset const all{supported_amendments()}; + FeatureBitset const all{testable_amendments()}; FeatureBitset const noAMM{all - featureAMM}; FeatureBitset const noNumber{all - fixUniversalNumber}; FeatureBitset const noAMMAndNumber{ @@ -4890,9 +5142,12 @@ private: carol, USD(100), std::nullopt, IOUAmount{520, 0}); // carol withdraws ~1,443.44USD auto const balanceAfterWithdraw = [&]() { - if (!features[fixAMMv1_1]) + if (!features[fixAMMv1_1] && !features[fixAMMv1_3]) return STAmount(USD, UINT64_C(30'443'43891402715), -11); - return STAmount(USD, UINT64_C(30'443'43891402714), -11); + else if (features[fixAMMv1_1] && !features[fixAMMv1_3]) + return STAmount(USD, UINT64_C(30'443'43891402714), -11); + else + return STAmount(USD, UINT64_C(30'443'43891402713), -11); }(); BEAST_EXPECT(env.balance(carol, USD) == balanceAfterWithdraw); // Set to original pool size @@ -4902,22 +5157,29 @@ private: ammAlice.vote(alice, 0); BEAST_EXPECT(ammAlice.expectTradingFee(0)); auto const tokensNoFee = ammAlice.withdraw(carol, deposit); - if (!features[fixAMMv1_1]) + if (!features[fixAMMv1_1] && !features[fixAMMv1_3]) BEAST_EXPECT( env.balance(carol, USD) == STAmount(USD, UINT64_C(30'443'43891402717), -11)); - else + else if (features[fixAMMv1_1] && !features[fixAMMv1_3]) BEAST_EXPECT( env.balance(carol, USD) == STAmount(USD, UINT64_C(30'443'43891402716), -11)); - // carol pays ~4008 LPTokens in fees or ~0.5% of the no-fee - // LPTokens - if (!features[fixAMMv1_1]) - BEAST_EXPECT( - tokensNoFee == IOUAmount(746'579'80779913, -8)); else + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(30'443'43891402713), -11)); + // carol pays ~4008 LPTokens in fees or ~0.5% of the no-fee + // LPTokens + if (!features[fixAMMv1_1] && !features[fixAMMv1_3]) + BEAST_EXPECT( + tokensNoFee == IOUAmount(746'579'80779913, -8)); + else if (features[fixAMMv1_1] && !features[fixAMMv1_3]) BEAST_EXPECT( tokensNoFee == IOUAmount(746'579'80779912, -8)); + else + BEAST_EXPECT( + tokensNoFee == IOUAmount(746'579'80779911, -8)); BEAST_EXPECT(tokensFee == IOUAmount(750'588'23529411, -8)); }, std::nullopt, @@ -5214,11 +5476,16 @@ private: // Due to round off some accounts have a tiny gain, while // other have a tiny loss. The last account to withdraw // gets everything in the pool. - if (!features[fixAMMv1_1]) + if (!features[fixAMMv1_1] && !features[fixAMMv1_3]) BEAST_EXPECT(ammAlice.expectBalances( XRP(10'000), STAmount{USD, UINT64_C(10'000'0000000013), -10}, IOUAmount{10'000'000})); + else if (features[fixAMMv1_3]) + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10'000), + STAmount{USD, UINT64_C(10'000'0000000003), -10}, + IOUAmount{10'000'000})); else BEAST_EXPECT(ammAlice.expectBalances( XRP(10'000), USD(10'000), IOUAmount{10'000'000})); @@ -5226,25 +5493,29 @@ private: BEAST_EXPECT(expectLine(env, simon, USD(1'500'000))); BEAST_EXPECT(expectLine(env, chris, USD(1'500'000))); BEAST_EXPECT(expectLine(env, dan, USD(1'500'000))); - if (!features[fixAMMv1_1]) + if (!features[fixAMMv1_1] && !features[fixAMMv1_3]) BEAST_EXPECT(expectLine( env, carol, STAmount{USD, UINT64_C(30'000'00000000001), -11})); + else if (features[fixAMMv1_1] && !features[fixAMMv1_3]) + BEAST_EXPECT(expectLine(env, carol, USD(30'000))); else BEAST_EXPECT(expectLine(env, carol, USD(30'000))); BEAST_EXPECT(expectLine(env, ed, USD(1'500'000))); BEAST_EXPECT(expectLine(env, paul, USD(1'500'000))); - if (!features[fixAMMv1_1]) + if (!features[fixAMMv1_1] && !features[fixAMMv1_3]) BEAST_EXPECT(expectLine( env, nataly, STAmount{USD, UINT64_C(1'500'000'000000002), -9})); - else + else if (features[fixAMMv1_1] && !features[fixAMMv1_3]) BEAST_EXPECT(expectLine( env, nataly, STAmount{USD, UINT64_C(1'500'000'000000005), -9})); + else + BEAST_EXPECT(expectLine(env, nataly, USD(1'500'000))); ammAlice.withdrawAll(alice); BEAST_EXPECT(!ammAlice.ammExists()); if (!features[fixAMMv1_1]) @@ -5252,6 +5523,11 @@ private: env, alice, STAmount{USD, UINT64_C(30'000'0000000013), -10})); + else if (features[fixAMMv1_3]) + BEAST_EXPECT(expectLine( + env, + alice, + STAmount{USD, UINT64_C(30'000'0000000003), -10})); else BEAST_EXPECT(expectLine(env, alice, USD(30'000))); // alice XRP balance is 30,000initial - 50 ammcreate fee - @@ -5267,68 +5543,110 @@ private: {features}); // Same as above but deposit/withdraw in XRP - testAMM([&](AMM& ammAlice, Env& env) { - Account const bob("bob"); - Account const ed("ed"); - Account const paul("paul"); - Account const dan("dan"); - Account const chris("chris"); - Account const simon("simon"); - Account const ben("ben"); - Account const nataly("nataly"); - fund( - env, - gw, - {bob, ed, paul, dan, chris, simon, ben, nataly}, - XRP(2'000'000), - {}, - Fund::Acct); - for (int i = 0; i < 10; ++i) - { - ammAlice.deposit(ben, XRPAmount{1}); - ammAlice.withdrawAll(ben, XRP(0)); - ammAlice.deposit(simon, XRPAmount(1'000)); - ammAlice.withdrawAll(simon, XRP(0)); - ammAlice.deposit(chris, XRP(1)); - ammAlice.withdrawAll(chris, XRP(0)); - ammAlice.deposit(dan, XRP(10)); - ammAlice.withdrawAll(dan, XRP(0)); - ammAlice.deposit(bob, XRP(100)); - ammAlice.withdrawAll(bob, XRP(0)); - ammAlice.deposit(carol, XRP(1'000)); - ammAlice.withdrawAll(carol, XRP(0)); - ammAlice.deposit(ed, XRP(10'000)); - ammAlice.withdrawAll(ed, XRP(0)); - ammAlice.deposit(paul, XRP(100'000)); - ammAlice.withdrawAll(paul, XRP(0)); - ammAlice.deposit(nataly, XRP(1'000'000)); - ammAlice.withdrawAll(nataly, XRP(0)); - } - // No round off with XRP in this test - BEAST_EXPECT(ammAlice.expectBalances( - XRP(10'000), USD(10'000), IOUAmount{10'000'000})); - ammAlice.withdrawAll(alice); - BEAST_EXPECT(!ammAlice.ammExists()); - // 20,000 initial - (deposit+withdraw) * 10 - auto const xrpBalance = (XRP(2'000'000) - txfee(env, 20)).getText(); - BEAST_EXPECT(accountBalance(env, ben) == xrpBalance); - BEAST_EXPECT(accountBalance(env, simon) == xrpBalance); - BEAST_EXPECT(accountBalance(env, chris) == xrpBalance); - BEAST_EXPECT(accountBalance(env, dan) == xrpBalance); + testAMM( + [&](AMM& ammAlice, Env& env) { + Account const bob("bob"); + Account const ed("ed"); + Account const paul("paul"); + Account const dan("dan"); + Account const chris("chris"); + Account const simon("simon"); + Account const ben("ben"); + Account const nataly("nataly"); + fund( + env, + gw, + {bob, ed, paul, dan, chris, simon, ben, nataly}, + XRP(2'000'000), + {}, + Fund::Acct); + for (int i = 0; i < 10; ++i) + { + ammAlice.deposit(ben, XRPAmount{1}); + ammAlice.withdrawAll(ben, XRP(0)); + ammAlice.deposit(simon, XRPAmount(1'000)); + ammAlice.withdrawAll(simon, XRP(0)); + ammAlice.deposit(chris, XRP(1)); + ammAlice.withdrawAll(chris, XRP(0)); + ammAlice.deposit(dan, XRP(10)); + ammAlice.withdrawAll(dan, XRP(0)); + ammAlice.deposit(bob, XRP(100)); + ammAlice.withdrawAll(bob, XRP(0)); + ammAlice.deposit(carol, XRP(1'000)); + ammAlice.withdrawAll(carol, XRP(0)); + ammAlice.deposit(ed, XRP(10'000)); + ammAlice.withdrawAll(ed, XRP(0)); + ammAlice.deposit(paul, XRP(100'000)); + ammAlice.withdrawAll(paul, XRP(0)); + ammAlice.deposit(nataly, XRP(1'000'000)); + ammAlice.withdrawAll(nataly, XRP(0)); + } + auto const baseFee = env.current()->fees().base.drops(); + if (!features[fixAMMv1_3]) + { + // No round off with XRP in this test + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10'000), USD(10'000), IOUAmount{10'000'000})); + ammAlice.withdrawAll(alice); + BEAST_EXPECT(!ammAlice.ammExists()); + // 20,000 initial - (deposit+withdraw) * 10 + auto const xrpBalance = + (XRP(2'000'000) - txfee(env, 20)).getText(); + BEAST_EXPECT(accountBalance(env, ben) == xrpBalance); + BEAST_EXPECT(accountBalance(env, simon) == xrpBalance); + BEAST_EXPECT(accountBalance(env, chris) == xrpBalance); + BEAST_EXPECT(accountBalance(env, dan) == xrpBalance); - auto const baseFee = env.current()->fees().base.drops(); - // 30,000 initial - (deposit+withdraw) * 10 - BEAST_EXPECT( - accountBalance(env, carol) == - std::to_string(30000000000 - 20 * baseFee)); - BEAST_EXPECT(accountBalance(env, ed) == xrpBalance); - BEAST_EXPECT(accountBalance(env, paul) == xrpBalance); - BEAST_EXPECT(accountBalance(env, nataly) == xrpBalance); - // 30,000 initial - 50 ammcreate fee - 10drops withdraw fee - BEAST_EXPECT( - accountBalance(env, alice) == - std::to_string(29950000000 - baseFee)); - }); + // 30,000 initial - (deposit+withdraw) * 10 + BEAST_EXPECT( + accountBalance(env, carol) == + std::to_string(30'000'000'000 - 20 * baseFee)); + BEAST_EXPECT(accountBalance(env, ed) == xrpBalance); + BEAST_EXPECT(accountBalance(env, paul) == xrpBalance); + BEAST_EXPECT(accountBalance(env, nataly) == xrpBalance); + // 30,000 initial - 50 ammcreate fee - 10drops withdraw fee + BEAST_EXPECT( + accountBalance(env, alice) == + std::to_string(29'950'000'000 - baseFee)); + } + else + { + // post-amendment the rounding takes place to ensure + // AMM invariant + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(10'000'000'080), + USD(10'000), + IOUAmount{10'000'000})); + ammAlice.withdrawAll(alice); + BEAST_EXPECT(!ammAlice.ammExists()); + auto const xrpBalance = + XRP(2'000'000) - txfee(env, 20) - drops(10); + auto const xrpBalanceText = xrpBalance.getText(); + BEAST_EXPECT(accountBalance(env, ben) == xrpBalanceText); + BEAST_EXPECT(accountBalance(env, simon) == xrpBalanceText); + BEAST_EXPECT(accountBalance(env, chris) == xrpBalanceText); + BEAST_EXPECT(accountBalance(env, dan) == xrpBalanceText); + BEAST_EXPECT( + accountBalance(env, carol) == + std::to_string(30'000'000'000 - 20 * baseFee - 10)); + BEAST_EXPECT( + accountBalance(env, ed) == + (xrpBalance + drops(2)).getText()); + BEAST_EXPECT( + accountBalance(env, paul) == + (xrpBalance + drops(3)).getText()); + BEAST_EXPECT( + accountBalance(env, nataly) == + (xrpBalance + drops(5)).getText()); + BEAST_EXPECT( + accountBalance(env, alice) == + std::to_string(29'950'000'000 - baseFee + 80)); + } + }, + std::nullopt, + 0, + std::nullopt, + {features}); } void @@ -5337,7 +5655,7 @@ private: testcase("Auto Delete"); using namespace jtx; - FeatureBitset const all{supported_amendments()}; + FeatureBitset const all{testable_amendments()}; { Env env( @@ -5997,7 +6315,7 @@ private: { testcase("Fix Default Inner Object"); using namespace jtx; - FeatureBitset const all{supported_amendments()}; + FeatureBitset const all{testable_amendments()}; auto test = [&](FeatureBitset features, TER const& err1, @@ -6370,11 +6688,11 @@ private: } void - testFixOverflowOffer(FeatureBitset features) + testFixOverflowOffer(FeatureBitset featuresInitial) { using namespace jtx; using namespace std::chrono; - FeatureBitset const all{features}; + FeatureBitset const all{featuresInitial}; std::string logs; @@ -6401,6 +6719,7 @@ private: STAmount const goodUsdBIT; STAmount const goodUsdBITr; IOUAmount const lpTokenBalance; + std::optional const lpTokenBalanceAlt = {}; double const offer1BtcGH = 0.1; double const offer2BtcGH = 0.1; double const offer2UsdGH = 1; @@ -6426,6 +6745,7 @@ private: .goodUsdBIT{usdBIT, uint64_t(8'464739069120721), -15}, // .goodUsdBITr{usdBIT, uint64_t(8'464739069098152), -15}, // .lpTokenBalance = {28'61817604250837, -14}, // + .lpTokenBalanceAlt = IOUAmount{28'61817604250836, -14}, // .offer1BtcGH = 0.1, // .offer2BtcGH = 0.1, // .offer2UsdGH = 1, // @@ -6604,7 +6924,7 @@ private: { testcase(input.testCase); for (auto const& features : - {all - fixAMMOverflowOffer, all | fixAMMOverflowOffer}) + {all - fixAMMOverflowOffer - fixAMMv1_1 - fixAMMv1_3, all}) { Env env(*this, features, std::make_unique(&logs)); @@ -6658,15 +6978,19 @@ private: features[fixAMMv1_1] ? input.goodUsdGHr : input.goodUsdGH; auto const goodUsdBIT = features[fixAMMv1_1] ? input.goodUsdBITr : input.goodUsdBIT; + auto const lpTokenBalance = + env.enabled(fixAMMv1_3) && input.lpTokenBalanceAlt + ? *input.lpTokenBalanceAlt + : input.lpTokenBalance; if (!features[fixAMMOverflowOffer]) { BEAST_EXPECT(amm.expectBalances( - failUsdGH, failUsdBIT, input.lpTokenBalance)); + failUsdGH, failUsdBIT, lpTokenBalance)); } else { BEAST_EXPECT(amm.expectBalances( - goodUsdGH, goodUsdBIT, input.lpTokenBalance)); + goodUsdGH, goodUsdBIT, lpTokenBalance)); // Invariant: LPToken balance must not change in a // payment or a swap transaction @@ -6728,7 +7052,7 @@ private: {{xrpPool, iouPool}}, 889, std::nullopt, - {jtx::supported_amendments() | fixAMMv1_1}); + {jtx::testable_amendments() | fixAMMv1_1}); } void @@ -6862,11 +7186,13 @@ private: void testLPTokenBalance(FeatureBitset features) { + testcase("LPToken Balance"); using namespace jtx; // Last Liquidity Provider is the issuer of one token { - Env env(*this, features); + std::string logs; + Env env(*this, features, std::make_unique(&logs)); fund( env, gw, @@ -6877,7 +7203,9 @@ private: amm.deposit(alice, IOUAmount{1'876123487565916, -15}); amm.deposit(carol, IOUAmount{1'000'000}); amm.withdrawAll(alice); + BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{0})); amm.withdrawAll(carol); + BEAST_EXPECT(amm.expectLPTokens(carol, IOUAmount{0})); auto const lpToken = getAccountLines( env, gw, amm.lptIssue())[jss::lines][0u][jss::balance]; auto const lpTokenBalance = @@ -7003,7 +7331,8 @@ private: } // If featureAMMClawback is enabled, AMMCreate is allowed for // clawback-enabled issuer. Clawback from the AMM Account is not - // allowed, which will return tecAMM_ACCOUNT. We can only use + // allowed, which will return tecAMM_ACCOUNT or tecPSEUDO_ACCOUNT, + // depending on whether SingleAssetVault is enabled. We can only use // AMMClawback transaction to claw back from AMM Account. else { @@ -7014,13 +7343,16 @@ private: // By doing this, we make the clawback transaction's Amount field's // subfield `issuer` to be the AMM account, which means // we are clawing back from an AMM account. This should return an - // tecAMM_ACCOUNT error because regular Clawback transaction is not + // error because regular Clawback transaction is not // allowed for clawing back from an AMM account. Please notice the // `issuer` subfield represents the account being clawed back, which // is confusing. + auto const error = features[featureSingleAssetVault] + ? ter{tecPSEUDO_ACCOUNT} + : ter{tecAMM_ACCOUNT}; Issue usd(USD.issue().currency, amm.ammAccount()); auto amount = amountFromString(usd, "10"); - env(claw(gw, amount), ter(tecAMM_ACCOUNT)); + env(claw(gw, amount), error); } } @@ -7162,7 +7494,8 @@ private: auto const testCase = [&](std::string suffix, FeatureBitset features) { testcase("Fail pseudo-account allocation " + suffix); - Env env{*this, features}; + std::string logs; + Env env{*this, features, std::make_unique(&logs)}; env.fund(XRP(30'000), gw, alice); env.close(); env(trust(alice, gw["USD"](30'000), 0)); @@ -7193,16 +7526,399 @@ private: }; testCase( - "tecDUPLICATE", supported_amendments() - featureSingleAssetVault); + "tecDUPLICATE", testable_amendments() - featureSingleAssetVault); testCase( "terADDRESS_COLLISION", - supported_amendments() | featureSingleAssetVault); + testable_amendments() | featureSingleAssetVault); + } + + void + testDepositAndWithdrawRounding(FeatureBitset features) + { + testcase("Deposit and Withdraw Rounding V2"); + using namespace jtx; + + auto const XPM = gw["XPM"]; + STAmount xrpBalance{XRPAmount(692'614'492'126)}; + STAmount xpmBalance{XPM, UINT64_C(18'610'359'80246901), -8}; + STAmount amount{XPM, UINT64_C(6'566'496939465400), -12}; + std::uint16_t tfee = 941; + + auto test = [&](auto&& cb, std::uint16_t tfee_) { + Env env(*this, features); + env.fund(XRP(1'000'000), gw); + env.fund(XRP(1'000), alice); + env(trust(alice, XPM(7'000))); + env(pay(gw, alice, amount)); + + AMM amm(env, gw, xrpBalance, xpmBalance, CreateArg{.tfee = tfee_}); + // AMM LPToken balance required to replicate single deposit failure + STAmount lptAMMBalance{ + amm.lptIssue(), UINT64_C(3'234'987'266'485968), -6}; + auto const burn = + IOUAmount{amm.getLPTokensBalance() - lptAMMBalance}; + // burn tokens to get to the required AMM state + env(amm.bid(BidArg{.account = gw, .bidMin = burn, .bidMax = burn})); + cb(amm, env); + }; + test( + [&](AMM& amm, Env& env) { + auto const err = env.enabled(fixAMMv1_3) ? ter(tesSUCCESS) + : ter(tecUNFUNDED_AMM); + amm.deposit( + DepositArg{ + .account = alice, .asset1In = amount, .err = err}); + }, + tfee); + test( + [&](AMM& amm, Env& env) { + auto const [amount, amount2, lptAMM] = amm.balances(XRP, XPM); + auto const withdraw = STAmount{XPM, 1, -5}; + amm.withdraw(WithdrawArg{.asset1Out = STAmount{XPM, 1, -5}}); + auto const [amount_, amount2_, lptAMM_] = + amm.balances(XRP, XPM); + if (!env.enabled(fixAMMv1_3)) + BEAST_EXPECT((amount2 - amount2_) > withdraw); + else + BEAST_EXPECT((amount2 - amount2_) <= withdraw); + }, + 0); + } + + void + invariant( + jtx::AMM& amm, + jtx::Env& env, + std::string const& msg, + bool shouldFail) + { + auto const [amount, amount2, lptBalance] = amm.balances(GBP, EUR); + + NumberRoundModeGuard g( + env.enabled(fixAMMv1_3) ? Number::upward : Number::getround()); + auto const res = root2(amount * amount2); + + if (shouldFail) + BEAST_EXPECT(res < lptBalance); + else + BEAST_EXPECT(res >= lptBalance); + } + + void + testDepositRounding(FeatureBitset all) + { + testcase("Deposit Rounding"); + using namespace jtx; + + // Single asset deposit + for (auto const& deposit : + {STAmount(EUR, 1, 1), + STAmount(EUR, 1, 2), + STAmount(EUR, 1, 5), + STAmount(EUR, 1, -3), // fail + STAmount(EUR, 1, -6), + STAmount(EUR, 1, -9)}) + { + testAMM( + [&](AMM& ammAlice, Env& env) { + fund( + env, + gw, + {bob}, + XRP(10'000'000), + {GBP(100'000), EUR(100'000)}, + Fund::Acct); + env.close(); + + ammAlice.deposit( + DepositArg{.account = bob, .asset1In = deposit}); + invariant( + ammAlice, + env, + "dep1", + deposit == STAmount{EUR, 1, -3} && + !env.enabled(fixAMMv1_3)); + }, + {{GBP(30'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + } + + // Two-asset proportional deposit (1:1 pool ratio) + testAMM( + [&](AMM& ammAlice, Env& env) { + fund( + env, + gw, + {bob}, + XRP(10'000'000), + {GBP(100'000), EUR(100'000)}, + Fund::Acct); + env.close(); + + STAmount const depositEuro{ + EUR, UINT64_C(10'1234567890123456), -16}; + STAmount const depositGBP{ + GBP, UINT64_C(10'1234567890123456), -16}; + + ammAlice.deposit( + DepositArg{ + .account = bob, + .asset1In = depositEuro, + .asset2In = depositGBP}); + invariant(ammAlice, env, "dep2", false); + }, + {{GBP(30'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + + // Two-asset proportional deposit (1:3 pool ratio) + for (auto const& exponent : {1, 2, 3, 4, -3 /*fail*/, -6, -9}) + { + testAMM( + [&](AMM& ammAlice, Env& env) { + fund( + env, + gw, + {bob}, + XRP(10'000'000), + {GBP(100'000), EUR(100'000)}, + Fund::Acct); + env.close(); + + STAmount const depositEuro{EUR, 1, exponent}; + STAmount const depositGBP{GBP, 1, exponent}; + + ammAlice.deposit( + DepositArg{ + .account = bob, + .asset1In = depositEuro, + .asset2In = depositGBP}); + invariant( + ammAlice, + env, + "dep3", + exponent != -3 && !env.enabled(fixAMMv1_3)); + }, + {{GBP(10'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + } + + // tfLPToken deposit + testAMM( + [&](AMM& ammAlice, Env& env) { + fund( + env, + gw, + {bob}, + XRP(10'000'000), + {GBP(100'000), EUR(100'000)}, + Fund::Acct); + env.close(); + + ammAlice.deposit( + DepositArg{ + .account = bob, + .tokens = IOUAmount{10'1234567890123456, -16}}); + invariant(ammAlice, env, "dep4", false); + }, + {{GBP(7'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + + // tfOneAssetLPToken deposit + for (auto const& tokens : + {IOUAmount{1, -3}, + IOUAmount{1, -2}, + IOUAmount{1, -1}, + IOUAmount{1}, + IOUAmount{10}, + IOUAmount{100}, + IOUAmount{1'000}, + IOUAmount{10'000}}) + { + testAMM( + [&](AMM& ammAlice, Env& env) { + fund( + env, + gw, + {bob}, + XRP(10'000'000), + {GBP(100'000), EUR(1'000'000)}, + Fund::Acct); + env.close(); + + ammAlice.deposit( + DepositArg{ + .account = bob, + .tokens = tokens, + .asset1In = STAmount{EUR, 1, 6}}); + invariant(ammAlice, env, "dep5", false); + }, + {{GBP(7'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + } + + // Single deposit with EP not exceeding specified: + // 1'000 GBP with EP not to exceed 5 (GBP/TokensOut) + testAMM( + [&](AMM& ammAlice, Env& env) { + fund( + env, + gw, + {bob}, + XRP(10'000'000), + {GBP(100'000), EUR(100'000)}, + Fund::Acct); + env.close(); + + ammAlice.deposit( + bob, GBP(1'000), std::nullopt, STAmount{GBP, 5}); + invariant(ammAlice, env, "dep6", false); + }, + {{GBP(30'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + } + + void + testWithdrawRounding(FeatureBitset all) + { + testcase("Withdraw Rounding"); + + using namespace jtx; + + // tfLPToken mode + testAMM( + [&](AMM& ammAlice, Env& env) { + ammAlice.withdraw(alice, 1'000); + invariant(ammAlice, env, "with1", false); + }, + {{GBP(7'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + + // tfWithdrawAll mode + testAMM( + [&](AMM& ammAlice, Env& env) { + ammAlice.withdraw( + WithdrawArg{.account = alice, .flags = tfWithdrawAll}); + invariant(ammAlice, env, "with2", false); + }, + {{GBP(7'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + + // tfTwoAsset withdraw mode + testAMM( + [&](AMM& ammAlice, Env& env) { + ammAlice.withdraw( + WithdrawArg{ + .account = alice, + .asset1Out = STAmount{GBP, 3'500}, + .asset2Out = STAmount{EUR, 15'000}, + .flags = tfTwoAsset}); + invariant(ammAlice, env, "with3", false); + }, + {{GBP(7'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + + // tfSingleAsset withdraw mode + // Note: This test fails with 0 trading fees, but doesn't fail if + // trading fees is set to 1'000 -- I suspect the compound operations + // in AMMHelpers.cpp:withdrawByTokens compensate for the rounding + // errors + testAMM( + [&](AMM& ammAlice, Env& env) { + ammAlice.withdraw( + WithdrawArg{ + .account = alice, + .asset1Out = STAmount{GBP, 1'234}, + .flags = tfSingleAsset}); + invariant(ammAlice, env, "with4", false); + }, + {{GBP(7'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + + // tfOneAssetWithdrawAll mode + testAMM( + [&](AMM& ammAlice, Env& env) { + fund( + env, + gw, + {bob}, + XRP(10'000'000), + {GBP(100'000), EUR(100'000)}, + Fund::Acct); + env.close(); + + ammAlice.deposit( + DepositArg{ + .account = bob, .asset1In = STAmount{GBP, 3'456}}); + + ammAlice.withdraw( + WithdrawArg{ + .account = bob, + .asset1Out = STAmount{GBP, 1'000}, + .flags = tfOneAssetWithdrawAll}); + invariant(ammAlice, env, "with5", false); + }, + {{GBP(7'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + + // tfOneAssetLPToken mode + testAMM( + [&](AMM& ammAlice, Env& env) { + ammAlice.withdraw( + WithdrawArg{ + .account = alice, + .tokens = 1'000, + .asset1Out = STAmount{GBP, 100}, + .flags = tfOneAssetLPToken}); + invariant(ammAlice, env, "with6", false); + }, + {{GBP(7'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + + // tfLimitLPToken mode + testAMM( + [&](AMM& ammAlice, Env& env) { + ammAlice.withdraw( + WithdrawArg{ + .account = alice, + .asset1Out = STAmount{GBP, 100}, + .maxEP = IOUAmount{2}, + .flags = tfLimitLPToken}); + invariant(ammAlice, env, "with7", true); + }, + {{GBP(7'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); } void run() override { - FeatureBitset const all{jtx::supported_amendments()}; + FeatureBitset const all{jtx::testable_amendments()}; testInvalidInstance(); testInstanceCreate(); testInvalidDeposit(all); @@ -7214,46 +7930,62 @@ private: testFeeVote(); testInvalidBid(); testBid(all); - testBid(all - fixAMMv1_1); + testBid(all - fixAMMv1_3); + testBid(all - fixAMMv1_1 - fixAMMv1_3); testInvalidAMMPayment(); testBasicPaymentEngine(all); - testBasicPaymentEngine(all - fixAMMv1_1); + testBasicPaymentEngine(all - fixAMMv1_1 - fixAMMv1_3); testBasicPaymentEngine(all - fixReducedOffersV2); - testBasicPaymentEngine(all - fixAMMv1_1 - fixReducedOffersV2); + testBasicPaymentEngine( + all - fixAMMv1_1 - fixAMMv1_3 - fixReducedOffersV2); testAMMTokens(); testAmendment(); testFlags(); testRippling(); testAMMAndCLOB(all); - testAMMAndCLOB(all - fixAMMv1_1); + testAMMAndCLOB(all - fixAMMv1_1 - fixAMMv1_3); testTradingFee(all); - testTradingFee(all - fixAMMv1_1); + testTradingFee(all - fixAMMv1_3); + testTradingFee(all - fixAMMv1_1 - fixAMMv1_3); testAdjustedTokens(all); - testAdjustedTokens(all - fixAMMv1_1); + testAdjustedTokens(all - fixAMMv1_3); + testAdjustedTokens(all - fixAMMv1_1 - fixAMMv1_3); testAutoDelete(); testClawback(); testAMMID(); testSelection(all); - testSelection(all - fixAMMv1_1); + testSelection(all - fixAMMv1_1 - fixAMMv1_3); testFixDefaultInnerObj(); testMalformed(); testFixOverflowOffer(all); - testFixOverflowOffer(all - fixAMMv1_1); + testFixOverflowOffer(all - fixAMMv1_3); + testFixOverflowOffer(all - fixAMMv1_1 - fixAMMv1_3); testSwapRounding(); testFixChangeSpotPriceQuality(all); - testFixChangeSpotPriceQuality(all - fixAMMv1_1); + testFixChangeSpotPriceQuality(all - fixAMMv1_1 - fixAMMv1_3); testFixAMMOfferBlockedByLOB(all); - testFixAMMOfferBlockedByLOB(all - fixAMMv1_1); + testFixAMMOfferBlockedByLOB(all - fixAMMv1_1 - fixAMMv1_3); testLPTokenBalance(all); - testLPTokenBalance(all - fixAMMv1_1); + testLPTokenBalance(all - fixAMMv1_3); + testLPTokenBalance(all - fixAMMv1_1 - fixAMMv1_3); testAMMClawback(all); + testAMMClawback(all - featureSingleAssetVault); + testAMMClawback(all - featureAMMClawback - featureSingleAssetVault); testAMMClawback(all - featureAMMClawback); - testAMMClawback(all - fixAMMv1_1 - featureAMMClawback); + testAMMClawback(all - fixAMMv1_1 - fixAMMv1_3 - featureAMMClawback); testAMMDepositWithFrozenAssets(all); testAMMDepositWithFrozenAssets(all - featureAMMClawback); testAMMDepositWithFrozenAssets(all - fixAMMv1_1 - featureAMMClawback); + testAMMDepositWithFrozenAssets( + all - fixAMMv1_1 - fixAMMv1_3 - featureAMMClawback); testFixReserveCheckOnWithdrawal(all); testFixReserveCheckOnWithdrawal(all - fixAMMv1_2); + testDepositAndWithdrawRounding(all); + testDepositAndWithdrawRounding(all - fixAMMv1_3); + testDepositRounding(all); + testDepositRounding(all - fixAMMv1_3); + testWithdrawRounding(all); + testWithdrawRounding(all - fixAMMv1_3); testFailedPseudoAccount(); } }; diff --git a/src/test/app/AccountDelete_test.cpp b/src/test/app/AccountDelete_test.cpp index 4ae18d9d28..f7c4ddc509 100644 --- a/src/test/app/AccountDelete_test.cpp +++ b/src/test/app/AccountDelete_test.cpp @@ -28,12 +28,6 @@ namespace test { class AccountDelete_test : public beast::unit_test::suite { private: - std::uint32_t - openLedgerSeq(jtx::Env& env) - { - return env.current()->seq(); - } - // Helper function that verifies the expected DeliveredAmount is present. // // NOTE: the function _infers_ the transaction to operate on by calling @@ -83,26 +77,6 @@ private: return jv; }; - // Close the ledger until the ledger sequence is large enough to close - // the account. If margin is specified, close the ledger so `margin` - // more closes are needed - void - incLgrSeqForAccDel( - jtx::Env& env, - jtx::Account const& acc, - std::uint32_t margin = 0) - { - int const delta = [&]() -> int { - if (env.seq(acc) + 255 > openLedgerSeq(env)) - return env.seq(acc) - openLedgerSeq(env) + 255 - margin; - return 0; - }(); - BEAST_EXPECT(margin == 0 || delta >= 0); - for (int i = 0; i < delta; ++i) - env.close(); - BEAST_EXPECT(openLedgerSeq(env) == env.seq(acc) + 255 - margin); - } - public: void testBasics() @@ -318,7 +292,7 @@ public: // o New-styled PayChannels with the backlink. // So we start the test using old-style PayChannels. Then we pass // the amendment to get new-style PayChannels. - Env env{*this, supported_amendments() - fixPayChanRecipientOwnerDir}; + Env env{*this, testable_amendments() - fixPayChanRecipientOwnerDir}; Account const alice("alice"); Account const becky("becky"); Account const gw("gw"); @@ -361,27 +335,11 @@ public: env(check::cancel(becky, checkId)); env.close(); - // Lambda to create an escrow. - auto escrowCreate = [](jtx::Account const& account, - jtx::Account const& to, - STAmount const& amount, - NetClock::time_point const& cancelAfter) { - Json::Value jv; - jv[jss::TransactionType] = jss::EscrowCreate; - jv[jss::Flags] = tfUniversal; - jv[jss::Account] = account.human(); - jv[jss::Destination] = to.human(); - jv[jss::Amount] = amount.getJson(JsonOptions::none); - jv[sfFinishAfter.jsonName] = - cancelAfter.time_since_epoch().count() + 1; - jv[sfCancelAfter.jsonName] = - cancelAfter.time_since_epoch().count() + 2; - return jv; - }; - using namespace std::chrono_literals; std::uint32_t const escrowSeq{env.seq(alice)}; - env(escrowCreate(alice, becky, XRP(333), env.now() + 2s)); + env(escrow::create(alice, becky, XRP(333)), + escrow::finish_time(env.now() + 3s), + escrow::cancel_time(env.now() + 4s)); env.close(); // alice and becky should be unable to delete their accounts because @@ -393,18 +351,39 @@ public: // Now cancel the escrow, but create a payment channel between // alice and becky. - // Lambda to cancel an escrow. - auto escrowCancel = - [](Account const& account, Account const& from, std::uint32_t seq) { - Json::Value jv; - jv[jss::TransactionType] = jss::EscrowCancel; - jv[jss::Flags] = tfUniversal; - jv[jss::Account] = account.human(); - jv[sfOwner.jsonName] = from.human(); - jv[sfOfferSequence.jsonName] = seq; - return jv; - }; - env(escrowCancel(becky, alice, escrowSeq)); + bool const withTokenEscrow = + env.current()->rules().enabled(featureTokenEscrow); + if (withTokenEscrow) + { + Account const gw1("gw1"); + Account const carol("carol"); + auto const USD = gw1["USD"]; + env.fund(XRP(100000), carol, gw1); + env(fset(gw1, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10000), carol); + env.close(); + env(pay(gw1, carol, USD(100))); + env.close(); + + std::uint32_t const escrowSeq{env.seq(carol)}; + env(escrow::create(carol, becky, USD(1)), + escrow::finish_time(env.now() + 3s), + escrow::cancel_time(env.now() + 4s)); + env.close(); + + incLgrSeqForAccDel(env, gw1); + + env(acctdelete(gw1, becky), + fee(acctDelFee), + ter(tecHAS_OBLIGATIONS)); + env.close(); + + env(escrow::cancel(becky, carol, escrowSeq)); + env.close(); + } + + env(escrow::cancel(becky, alice, escrowSeq)); env.close(); Keylet const alicePayChanKey{ @@ -482,7 +461,7 @@ public: // We need an old-style PayChannel that doesn't provide a backlink // from the destination. So don't enable the amendment with that fix. - Env env{*this, supported_amendments() - fixPayChanRecipientOwnerDir}; + Env env{*this, testable_amendments() - fixPayChanRecipientOwnerDir}; Account const alice("alice"); Account const becky("becky"); @@ -536,7 +515,6 @@ public: auto payChanClaim = [&]() { Json::Value jv; jv[jss::TransactionType] = jss::PaymentChannelClaim; - jv[jss::Flags] = tfUniversal; jv[jss::Account] = alice.human(); jv[sfChannel.jsonName] = to_string(payChanKey.key); jv[sfBalance.jsonName] = @@ -558,7 +536,7 @@ public: testcase("Amendment enable"); - Env env{*this, supported_amendments() - featureDeletableAccounts}; + Env env{*this, testable_amendments() - featureDeletableAccounts}; Account const alice("alice"); Account const becky("becky"); @@ -1150,7 +1128,7 @@ public: Account const becky{"becky"}; Account const carol{"carol"}; - Env env{*this, supported_amendments() - featureCredentials}; + Env env{*this, testable_amendments() - featureCredentials}; env.fund(XRP(100000), alice, becky, carol); env.close(); diff --git a/src/test/app/AmendmentTable_test.cpp b/src/test/app/AmendmentTable_test.cpp index 5ba820da95..407b2fafe1 100644 --- a/src/test/app/AmendmentTable_test.cpp +++ b/src/test/app/AmendmentTable_test.cpp @@ -1288,7 +1288,7 @@ public: void run() override { - FeatureBitset const all{test::jtx::supported_amendments()}; + FeatureBitset const all{test::jtx::testable_amendments()}; FeatureBitset const fixMajorityCalc{fixAmendmentMajorityCalc}; testConstruct(); diff --git a/src/test/app/Batch_test.cpp b/src/test/app/Batch_test.cpp new file mode 100644 index 0000000000..c8fcc4092b --- /dev/null +++ b/src/test/app/Batch_test.cpp @@ -0,0 +1,4175 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace ripple { +namespace test { + +class Batch_test : public beast::unit_test::suite +{ + struct TestLedgerData + { + int index; + std::string txType; + std::string result; + std::string txHash; + std::optional batchID; + }; + + struct TestBatchData + { + std::string result; + std::string txHash; + }; + + Json::Value + getTxByIndex(Json::Value const& jrr, int const index) + { + for (auto const& txn : jrr[jss::result][jss::ledger][jss::transactions]) + { + if (txn[jss::metaData][sfTransactionIndex.jsonName] == index) + return txn; + } + return {}; + } + + Json::Value + getLastLedger(jtx::Env& env) + { + Json::Value params; + params[jss::ledger_index] = env.closed()->seq(); + params[jss::transactions] = true; + params[jss::expand] = true; + return env.rpc("json", "ledger", to_string(params)); + } + + void + validateInnerTxn( + jtx::Env& env, + std::string const& batchID, + TestLedgerData const& ledgerResult) + { + Json::Value const jrr = env.rpc("tx", ledgerResult.txHash)[jss::result]; + BEAST_EXPECT(jrr[sfTransactionType.jsonName] == ledgerResult.txType); + BEAST_EXPECT( + jrr[jss::meta][sfTransactionResult.jsonName] == + ledgerResult.result); + BEAST_EXPECT(jrr[jss::meta][sfParentBatchID.jsonName] == batchID); + } + + void + validateClosedLedger( + jtx::Env& env, + std::vector const& ledgerResults) + { + auto const jrr = getLastLedger(env); + auto const transactions = + jrr[jss::result][jss::ledger][jss::transactions]; + BEAST_EXPECT(transactions.size() == ledgerResults.size()); + for (TestLedgerData const& ledgerResult : ledgerResults) + { + auto const txn = getTxByIndex(jrr, ledgerResult.index); + BEAST_EXPECT(txn[jss::hash].asString() == ledgerResult.txHash); + BEAST_EXPECT(txn.isMember(jss::metaData)); + Json::Value const meta = txn[jss::metaData]; + BEAST_EXPECT( + txn[sfTransactionType.jsonName] == ledgerResult.txType); + BEAST_EXPECT( + meta[sfTransactionResult.jsonName] == ledgerResult.result); + if (ledgerResult.batchID) + validateInnerTxn(env, *ledgerResult.batchID, ledgerResult); + } + } + + template + std::pair, std::string> + submitBatch(jtx::Env& env, TER const& result, Args&&... args) + { + auto batchTxn = env.jt(std::forward(args)...); + env(batchTxn, jtx::ter(result)); + + auto const ids = batchTxn.stx->getBatchTransactionIDs(); + std::vector txIDs; + for (auto const& id : ids) + txIDs.push_back(strHex(id)); + TxID const batchID = batchTxn.stx->getTransactionID(); + return std::make_pair(txIDs, strHex(batchID)); + } + + static uint256 + getCheckIndex(AccountID const& account, std::uint32_t uSequence) + { + return keylet::check(account, uSequence).key; + } + + static std::unique_ptr + makeSmallQueueConfig( + std::map extraTxQ = {}, + std::map extraVoting = {}) + { + auto p = test::jtx::envconfig(); + auto& section = p->section("transaction_queue"); + section.set("ledgers_in_queue", "2"); + section.set("minimum_queue_size", "2"); + section.set("min_ledgers_to_compute_size_limit", "3"); + section.set("max_ledger_counts_to_store", "100"); + section.set("retry_sequence_percent", "25"); + section.set("normal_consensus_increase_percent", "0"); + + for (auto const& [k, v] : extraTxQ) + section.set(k, v); + + return p; + } + + auto + openLedgerFee(jtx::Env& env, XRPAmount const& batchFee) + { + using namespace jtx; + + auto const& view = *env.current(); + auto metrics = env.app().getTxQ().getMetrics(view); + return toDrops(metrics.openLedgerFeeLevel, batchFee) + 1; + } + + void + testEnable(FeatureBitset features) + { + testcase("enabled"); + + using namespace test::jtx; + using namespace std::literals; + + for (bool const withBatch : {true, false}) + { + auto const amend = withBatch ? features : features - featureBatch; + test::jtx::Env env{*this, envconfig(), amend}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + env.fund(XRP(10000), alice, bob, carol); + env.close(); + + // ttBatch + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const txResult = + withBatch ? ter(tesSUCCESS) : ter(temDISABLED); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(1)), seq + 2), + txResult); + env.close(); + } + + // tfInnerBatchTxn + // If the feature is disabled, the transaction fails with + // temINVALID_FLAG If the feature is enabled, the transaction fails + // early in checkValidity() + { + auto const txResult = + withBatch ? ter(telENV_RPC_FAILED) : ter(temINVALID_FLAG); + env(pay(alice, bob, XRP(1)), + txflags(tfInnerBatchTxn), + txResult); + env.close(); + } + + env.close(); + } + } + + void + testPreflight(FeatureBitset features) + { + testcase("preflight"); + + using namespace test::jtx; + using namespace std::literals; + + //---------------------------------------------------------------------- + // preflight + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + env.fund(XRP(10000), alice, bob, carol); + env.close(); + + // temBAD_FEE: preflight1 + { + env(batch::outer(alice, env.seq(alice), XRP(-1), tfAllOrNothing), + ter(temBAD_FEE)); + env.close(); + } + + // DEFENSIVE: temINVALID_FLAG: Batch: inner batch flag. + // ACTUAL: telENV_RPC_FAILED: checkValidity() + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 0); + env(batch::outer(alice, seq, batchFee, tfInnerBatchTxn), + ter(telENV_RPC_FAILED)); + env.close(); + } + + // temINVALID_FLAG: Batch: invalid flags. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 0); + env(batch::outer(alice, seq, batchFee, tfDisallowXRP), + ter(temINVALID_FLAG)); + env.close(); + } + + // temINVALID_FLAG: Batch: too many flags. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 0); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + txflags(tfAllOrNothing | tfOnlyOne), + ter(temINVALID_FLAG)); + env.close(); + } + + // temARRAY_EMPTY: Batch: txns array must have at least 2 entries. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 0); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + ter(temARRAY_EMPTY)); + env.close(); + } + + // temARRAY_EMPTY: Batch: txns array must have at least 2 entries. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 0); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + ter(temARRAY_EMPTY)); + env.close(); + } + + // DEFENSIVE: temARRAY_TOO_LARGE: Batch: txns array exceeds 8 entries. + // ACTUAL: telENV_RPC_FAILED: isRawTransactionOkay() + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 9); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(1)), seq + 2), + batch::inner(pay(alice, bob, XRP(1)), seq + 3), + batch::inner(pay(alice, bob, XRP(1)), seq + 4), + batch::inner(pay(alice, bob, XRP(1)), seq + 5), + batch::inner(pay(alice, bob, XRP(1)), seq + 6), + batch::inner(pay(alice, bob, XRP(1)), seq + 7), + batch::inner(pay(alice, bob, XRP(1)), seq + 8), + batch::inner(pay(alice, bob, XRP(1)), seq + 9), + ter(telENV_RPC_FAILED)); + env.close(); + } + + // temREDUNDANT: Batch: duplicate Txn found. + { + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const seq = env.seq(alice); + auto jt = env.jtnofill( + batch::outer(alice, env.seq(alice), batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(alice, bob, XRP(10)), seq + 1)); + + env(jt.jv, batch::sig(bob), ter(temREDUNDANT)); + env.close(); + } + + // DEFENSIVE: temINVALID: Batch: batch cannot have inner batch txn. + // ACTUAL: telENV_RPC_FAILED: isRawTransactionOkay() + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner( + batch::outer(alice, seq, batchFee, tfAllOrNothing), seq), + batch::inner(pay(alice, bob, XRP(1)), seq + 2), + ter(telENV_RPC_FAILED)); + env.close(); + } + + // temINVALID_FLAG: Batch: inner txn must have the + // tfInnerBatchTxn flag. + { + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const seq = env.seq(alice); + auto tx1 = batch::inner(pay(alice, bob, XRP(10)), seq + 1); + tx1[jss::Flags] = 0; + auto jt = env.jtnofill( + batch::outer(alice, seq, batchFee, tfAllOrNothing), + tx1, + batch::inner(pay(alice, bob, XRP(10)), seq + 2)); + + env(jt.jv, batch::sig(bob), ter(temINVALID_FLAG)); + env.close(); + } + + // temBAD_SIGNATURE: Batch: inner txn cannot include TxnSignature. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto jt = env.jt(pay(alice, bob, XRP(1))); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(jt.jv, seq + 1), + batch::inner(pay(alice, bob, XRP(1)), seq + 2), + ter(temBAD_SIGNATURE)); + env.close(); + } + + // temBAD_SIGNER: Batch: inner txn cannot include Signers. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto tx1 = pay(alice, bob, XRP(1)); + tx1[sfSigners.jsonName] = Json::arrayValue; + tx1[sfSigners.jsonName][0U][sfSigner.jsonName] = Json::objectValue; + tx1[sfSigners.jsonName][0U][sfSigner.jsonName][sfAccount.jsonName] = + alice.human(); + tx1[sfSigners.jsonName][0U][sfSigner.jsonName] + [sfSigningPubKey.jsonName] = strHex(alice.pk()); + tx1[sfSigners.jsonName][0U][sfSigner.jsonName] + [sfTxnSignature.jsonName] = "DEADBEEF"; + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(tx1, seq + 1), + batch::inner(pay(alice, bob, XRP(1)), seq + 2), + ter(temBAD_SIGNER)); + env.close(); + } + + // temBAD_REGKEY: Batch: inner txn must include empty + // SigningPubKey. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto tx1 = batch::inner(pay(alice, bob, XRP(1)), seq + 1); + tx1[jss::SigningPubKey] = strHex(alice.pk()); + auto jt = env.jtnofill( + batch::outer(alice, seq, batchFee, tfAllOrNothing), + tx1, + batch::inner(pay(alice, bob, XRP(1)), seq + 2)); + + env(jt.jv, ter(temBAD_REGKEY)); + env.close(); + } + + // temINVALID_INNER_BATCH: Batch: inner txn preflight failed. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + // amount can't be negative + batch::inner(pay(alice, bob, XRP(-1)), seq + 2), + ter(temINVALID_INNER_BATCH)); + env.close(); + } + + // temBAD_FEE: Batch: inner txn must have a fee of 0. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto tx1 = batch::inner(pay(alice, bob, XRP(1)), seq + 1); + tx1[jss::Fee] = to_string(env.current()->fees().base); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + tx1, + batch::inner(pay(alice, bob, XRP(2)), seq + 2), + ter(temBAD_FEE)); + env.close(); + } + + // temSEQ_AND_TICKET: Batch: inner txn cannot have both Sequence + // and TicketSequence. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto tx1 = batch::inner(pay(alice, bob, XRP(1)), 0, 1); + tx1[jss::Sequence] = seq + 1; + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + tx1, + batch::inner(pay(alice, bob, XRP(2)), seq + 2), + ter(temSEQ_AND_TICKET)); + env.close(); + } + + // temSEQ_AND_TICKET: Batch: inner txn must have either Sequence or + // TicketSequence. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), 0), + batch::inner(pay(alice, bob, XRP(2)), seq + 2), + ter(temSEQ_AND_TICKET)); + env.close(); + } + + // temREDUNDANT: Batch: duplicate sequence found: + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(2)), seq + 1), + ter(temREDUNDANT)); + env.close(); + } + + // temREDUNDANT: Batch: duplicate ticket found: + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), 0, seq + 1), + batch::inner(pay(alice, bob, XRP(2)), 0, seq + 1), + ter(temREDUNDANT)); + env.close(); + } + + // temREDUNDANT: Batch: duplicate ticket & sequence found: + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), 0, seq + 1), + batch::inner(pay(alice, bob, XRP(2)), seq + 1), + ter(temREDUNDANT)); + env.close(); + } + + // DEFENSIVE: temARRAY_TOO_LARGE: Batch: signers array exceeds 8 + // entries. + // ACTUAL: telENV_RPC_FAILED: isRawTransactionOkay() + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 9, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(alice, bob, XRP(5)), seq + 2), + batch::sig( + bob, + carol, + alice, + bob, + carol, + alice, + bob, + carol, + alice, + alice), + ter(telENV_RPC_FAILED)); + env.close(); + } + + // temBAD_SIGNER: Batch: signer cannot be the outer account + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 2, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::sig(alice, bob), + ter(temBAD_SIGNER)); + env.close(); + } + + // temREDUNDANT: Batch: duplicate signer found + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 2, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::sig(bob, bob), + ter(temREDUNDANT)); + env.close(); + } + + // temBAD_SIGNER: Batch: no account signature for inner txn. + // Note: Extra signature by bob + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(alice, bob, XRP(5)), seq + 2), + batch::sig(bob), + ter(temBAD_SIGNER)); + env.close(); + } + + // temBAD_SIGNER: Batch: no account signature for inner txn. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::sig(carol), + ter(temBAD_SIGNER)); + env.close(); + } + + // temBAD_SIGNATURE: Batch: invalid batch txn signature. + { + auto const seq = env.seq(alice); + auto const bobSeq = env.seq(bob); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto jt = env.jtnofill( + batch::outer(alice, env.seq(alice), batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), bobSeq)); + + Serializer msg; + serializeBatch( + msg, tfAllOrNothing, jt.stx->getBatchTransactionIDs()); + auto const sig = ripple::sign(bob.pk(), bob.sk(), msg.slice()); + jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName] + [sfAccount.jsonName] = bob.human(); + jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName] + [sfSigningPubKey.jsonName] = strHex(alice.pk()); + jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName] + [sfTxnSignature.jsonName] = + strHex(Slice{sig.data(), sig.size()}); + + env(jt.jv, ter(temBAD_SIGNATURE)); + env.close(); + } + + // temBAD_SIGNER: Batch: invalid batch signers. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 2, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::inner(pay(carol, alice, XRP(5)), env.seq(carol)), + batch::sig(bob), + ter(temBAD_SIGNER)); + env.close(); + } + } + + void + testPreclaim(FeatureBitset features) + { + testcase("preclaim"); + + using namespace test::jtx; + using namespace std::literals; + + //---------------------------------------------------------------------- + // preclaim + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const dave = Account("dave"); + auto const elsa = Account("elsa"); + auto const frank = Account("frank"); + auto const phantom = Account("phantom"); + env.memoize(phantom); + + env.fund(XRP(10000), alice, bob, carol, dave, elsa, frank); + env.close(); + + //---------------------------------------------------------------------- + // checkSign.checkSingleSign + + // tefBAD_AUTH: Bob is not authorized to sign for Alice + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 3, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(alice, bob, XRP(20)), seq + 2), + sig(bob), + ter(tefBAD_AUTH)); + env.close(); + } + + //---------------------------------------------------------------------- + // checkBatchSign.checkMultiSign + + // tefNOT_MULTI_SIGNING: SignersList not enabled + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 3, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::msig(bob, {dave, carol}), + ter(tefNOT_MULTI_SIGNING)); + env.close(); + } + + env(signers(alice, 2, {{bob, 1}, {carol, 1}})); + env.close(); + + env(signers(bob, 2, {{carol, 1}, {dave, 1}, {elsa, 1}})); + env.close(); + + // tefBAD_SIGNATURE: Account not in SignersList + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 3, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::msig(bob, {carol, frank}), + ter(tefBAD_SIGNATURE)); + env.close(); + } + + // tefBAD_SIGNATURE: Wrong publicKey type + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 3, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::msig(bob, {carol, Account("dave", KeyType::ed25519)}), + ter(tefBAD_SIGNATURE)); + env.close(); + } + + // tefMASTER_DISABLED: Master key disabled + { + env(regkey(elsa, frank)); + env(fset(elsa, asfDisableMaster), sig(elsa)); + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 3, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::msig(bob, {carol, elsa}), + ter(tefMASTER_DISABLED)); + env.close(); + } + + // tefBAD_SIGNATURE: Signer does not exist + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 3, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::msig(bob, {carol, phantom}), + ter(tefBAD_SIGNATURE)); + env.close(); + } + + // tefBAD_SIGNATURE: Signer has not enabled RegularKey + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 3, 2); + Account const davo{"davo", KeyType::ed25519}; + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::msig(bob, {carol, Reg{dave, davo}}), + ter(tefBAD_SIGNATURE)); + env.close(); + } + + // tefBAD_SIGNATURE: Wrong RegularKey Set + { + env(regkey(dave, frank)); + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 3, 2); + Account const davo{"davo", KeyType::ed25519}; + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::msig(bob, {carol, Reg{dave, davo}}), + ter(tefBAD_SIGNATURE)); + env.close(); + } + + // tefBAD_QUORUM + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 2, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::msig(bob, {carol}), + ter(tefBAD_QUORUM)); + env.close(); + } + + // tesSUCCESS: BatchSigners.Signers + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 3, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::msig(bob, {carol, dave}), + ter(tesSUCCESS)); + env.close(); + } + + // tesSUCCESS: Multisign + BatchSigners.Signers + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 4, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::msig(bob, {carol, dave}), + msig(bob, carol), + ter(tesSUCCESS)); + env.close(); + } + + //---------------------------------------------------------------------- + // checkBatchSign.checkSingleSign + + // tefBAD_AUTH: Inner Account is not signer + { + auto const ledSeq = env.current()->seq(); + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, phantom, XRP(1000)), seq + 1), + batch::inner(noop(phantom), ledSeq), + batch::sig(Reg{phantom, carol}), + ter(tefBAD_AUTH)); + env.close(); + } + + // tefBAD_AUTH: Account is not signer + { + auto const ledSeq = env.current()->seq(); + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1000)), seq + 1), + batch::inner(noop(bob), ledSeq), + batch::sig(Reg{bob, carol}), + ter(tefBAD_AUTH)); + env.close(); + } + + // tesSUCCESS: Signed With Regular Key + { + env(regkey(bob, carol)); + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(bob, alice, XRP(2)), env.seq(bob)), + batch::sig(Reg{bob, carol}), + ter(tesSUCCESS)); + env.close(); + } + + // tesSUCCESS: Signed With Master Key + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(bob, alice, XRP(2)), env.seq(bob)), + batch::sig(bob), + ter(tesSUCCESS)); + env.close(); + } + + // tefMASTER_DISABLED: Signed With Master Key Disabled + { + env(regkey(bob, carol)); + env(fset(bob, asfDisableMaster), sig(bob)); + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(bob, alice, XRP(2)), env.seq(bob)), + batch::sig(bob), + ter(tefMASTER_DISABLED)); + env.close(); + } + } + + void + testBadRawTxn(FeatureBitset features) + { + testcase("bad raw txn"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + env.fund(XRP(10000), alice, bob); + + // Invalid: sfTransactionType + { + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const seq = env.seq(alice); + auto tx1 = batch::inner(pay(alice, bob, XRP(10)), seq + 1); + tx1.removeMember(jss::TransactionType); + auto jt = env.jtnofill( + batch::outer(alice, seq, batchFee, tfAllOrNothing), + tx1, + batch::inner(pay(alice, bob, XRP(10)), seq + 2)); + + env(jt.jv, batch::sig(bob), ter(telENV_RPC_FAILED)); + env.close(); + } + + // Invalid: sfAccount + { + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const seq = env.seq(alice); + auto tx1 = batch::inner(pay(alice, bob, XRP(10)), seq + 1); + tx1.removeMember(jss::Account); + auto jt = env.jtnofill( + batch::outer(alice, seq, batchFee, tfAllOrNothing), + tx1, + batch::inner(pay(alice, bob, XRP(10)), seq + 2)); + + env(jt.jv, batch::sig(bob), ter(telENV_RPC_FAILED)); + env.close(); + } + + // Invalid: sfSequence + { + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const seq = env.seq(alice); + auto tx1 = batch::inner(pay(alice, bob, XRP(10)), seq + 1); + tx1.removeMember(jss::Sequence); + auto jt = env.jtnofill( + batch::outer(alice, seq, batchFee, tfAllOrNothing), + tx1, + batch::inner(pay(alice, bob, XRP(10)), seq + 2)); + + env(jt.jv, batch::sig(bob), ter(telENV_RPC_FAILED)); + env.close(); + } + + // Invalid: sfFee + { + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const seq = env.seq(alice); + auto tx1 = batch::inner(pay(alice, bob, XRP(10)), seq + 1); + tx1.removeMember(jss::Fee); + auto jt = env.jtnofill( + batch::outer(alice, seq, batchFee, tfAllOrNothing), + tx1, + batch::inner(pay(alice, bob, XRP(10)), seq + 2)); + + env(jt.jv, batch::sig(bob), ter(telENV_RPC_FAILED)); + env.close(); + } + + // Invalid: sfSigningPubKey + { + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const seq = env.seq(alice); + auto tx1 = batch::inner(pay(alice, bob, XRP(10)), seq + 1); + tx1.removeMember(jss::SigningPubKey); + auto jt = env.jtnofill( + batch::outer(alice, seq, batchFee, tfAllOrNothing), + tx1, + batch::inner(pay(alice, bob, XRP(10)), seq + 2)); + + env(jt.jv, batch::sig(bob), ter(telENV_RPC_FAILED)); + env.close(); + } + } + + void + testBadSequence(FeatureBitset features) + { + testcase("bad sequence"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + env.fund(XRP(10000), alice, bob, gw); + env.close(); + env.trust(USD(1000), alice, bob); + env(pay(gw, alice, USD(100))); + env(pay(gw, bob, USD(100))); + env.close(); + + env(noop(bob), ter(tesSUCCESS)); + env.close(); + + // Invalid: Alice Sequence is a past sequence + { + auto const preAliceSeq = env.seq(alice); + auto const preAlice = env.balance(alice); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBobSeq = env.seq(bob); + auto const preBob = env.balance(bob); + auto const preBobUSD = env.balance(bob, USD.issue()); + + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), preAliceSeq - 10), + batch::inner(pay(bob, alice, XRP(5)), preBobSeq), + batch::sig(bob)); + + env.close(); + { + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + }; + validateClosedLedger(env, testCases); + } + + env.close(); + { + // next ledger is empty + std::vector testCases = {}; + validateClosedLedger(env, testCases); + } + + // Alice pays fee & Bob should not be affected. + BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1); + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD); + BEAST_EXPECT(env.seq(bob) == preBobSeq); + BEAST_EXPECT(env.balance(bob) == preBob); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD); + } + + // Invalid: Alice Sequence is a future sequence + { + auto const preAliceSeq = env.seq(alice); + auto const preAlice = env.balance(alice); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBobSeq = env.seq(bob); + auto const preBob = env.balance(bob); + auto const preBobUSD = env.balance(bob, USD.issue()); + + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), preAliceSeq + 10), + batch::inner(pay(bob, alice, XRP(5)), preBobSeq), + batch::sig(bob)); + + env.close(); + { + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + }; + validateClosedLedger(env, testCases); + } + + env.close(); + { + // next ledger is empty + std::vector testCases = {}; + validateClosedLedger(env, testCases); + } + + // Alice pays fee & Bob should not be affected. + BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1); + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD); + BEAST_EXPECT(env.seq(bob) == preBobSeq); + BEAST_EXPECT(env.balance(bob) == preBob); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD); + } + + // Invalid: Bob Sequence is a past sequence + { + auto const preAliceSeq = env.seq(alice); + auto const preAlice = env.balance(alice); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBobSeq = env.seq(bob); + auto const preBob = env.balance(bob); + auto const preBobUSD = env.balance(bob, USD.issue()); + + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), preAliceSeq + 1), + batch::inner(pay(bob, alice, XRP(5)), preBobSeq - 10), + batch::sig(bob)); + + env.close(); + { + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + }; + validateClosedLedger(env, testCases); + } + + env.close(); + { + // next ledger is empty + std::vector testCases = {}; + validateClosedLedger(env, testCases); + } + + // Alice pays fee & Bob should not be affected. + BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1); + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD); + BEAST_EXPECT(env.seq(bob) == preBobSeq); + BEAST_EXPECT(env.balance(bob) == preBob); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD); + } + + // Invalid: Bob Sequence is a future sequence + { + auto const preAliceSeq = env.seq(alice); + auto const preAlice = env.balance(alice); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBobSeq = env.seq(bob); + auto const preBob = env.balance(bob); + auto const preBobUSD = env.balance(bob, USD.issue()); + + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), preAliceSeq + 1), + batch::inner(pay(bob, alice, XRP(5)), preBobSeq + 10), + batch::sig(bob)); + + env.close(); + { + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + }; + validateClosedLedger(env, testCases); + } + + env.close(); + { + // next ledger is empty + std::vector testCases = {}; + validateClosedLedger(env, testCases); + } + + // Alice pays fee & Bob should not be affected. + BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1); + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD); + BEAST_EXPECT(env.seq(bob) == preBobSeq); + BEAST_EXPECT(env.balance(bob) == preBob); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD); + } + + // Invalid: Outer and Inner Sequence are the same + { + auto const preAliceSeq = env.seq(alice); + auto const preAlice = env.balance(alice); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBobSeq = env.seq(bob); + auto const preBob = env.balance(bob); + auto const preBobUSD = env.balance(bob, USD.issue()); + + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), preAliceSeq), + batch::inner(pay(bob, alice, XRP(5)), preBobSeq), + batch::sig(bob)); + + env.close(); + { + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + }; + validateClosedLedger(env, testCases); + } + + env.close(); + { + // next ledger is empty + std::vector testCases = {}; + validateClosedLedger(env, testCases); + } + + // Alice pays fee & Bob should not be affected. + BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1); + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD); + BEAST_EXPECT(env.seq(bob) == preBobSeq); + BEAST_EXPECT(env.balance(bob) == preBob); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD); + } + } + + void + testBadOuterFee(FeatureBitset features) + { + testcase("bad outer fee"); + + using namespace test::jtx; + using namespace std::literals; + + // Bad Fee Without Signer + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + env(noop(bob), ter(tesSUCCESS)); + env.close(); + + // Bad Fee: Should be batch::calcBatchFee(env, 0, 2) + auto const batchFee = batch::calcBatchFee(env, 0, 1); + auto const aliceSeq = env.seq(alice); + env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1), + batch::inner(pay(alice, bob, XRP(15)), aliceSeq + 2), + ter(telINSUF_FEE_P)); + env.close(); + } + + // Bad Fee With MultiSign + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + env.fund(XRP(10000), alice, bob, carol); + env.close(); + + env(noop(bob), ter(tesSUCCESS)); + env.close(); + + env(signers(alice, 2, {{bob, 1}, {carol, 1}})); + env.close(); + + // Bad Fee: Should be batch::calcBatchFee(env, 2, 2) + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const aliceSeq = env.seq(alice); + env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1), + batch::inner(pay(alice, bob, XRP(15)), aliceSeq + 2), + msig(bob, carol), + ter(telINSUF_FEE_P)); + env.close(); + } + + // Bad Fee With MultiSign + BatchSigners + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + env.fund(XRP(10000), alice, bob, carol); + env.close(); + + env(noop(bob), ter(tesSUCCESS)); + env.close(); + + env(signers(alice, 2, {{bob, 1}, {carol, 1}})); + env.close(); + + // Bad Fee: Should be batch::calcBatchFee(env, 3, 2) + auto const batchFee = batch::calcBatchFee(env, 2, 2); + auto const aliceSeq = env.seq(alice); + auto const bobSeq = env.seq(bob); + env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1), + batch::inner(pay(bob, alice, XRP(5)), bobSeq), + batch::sig(bob), + msig(bob, carol), + ter(telINSUF_FEE_P)); + env.close(); + } + + // Bad Fee With MultiSign + BatchSigners.Signers + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + env.fund(XRP(10000), alice, bob, carol); + env.close(); + + env(noop(bob), ter(tesSUCCESS)); + env.close(); + + env(signers(alice, 2, {{bob, 1}, {carol, 1}})); + env.close(); + + env(signers(bob, 2, {{alice, 1}, {carol, 1}})); + env.close(); + + // Bad Fee: Should be batch::calcBatchFee(env, 4, 2) + auto const batchFee = batch::calcBatchFee(env, 3, 2); + auto const aliceSeq = env.seq(alice); + auto const bobSeq = env.seq(bob); + env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1), + batch::inner(pay(bob, alice, XRP(5)), bobSeq), + batch::msig(bob, {alice, carol}), + msig(bob, carol), + ter(telINSUF_FEE_P)); + env.close(); + } + + // Bad Fee With BatchSigners + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + env(noop(bob), ter(tesSUCCESS)); + env.close(); + + // Bad Fee: Should be batch::calcBatchFee(env, 1, 2) + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const aliceSeq = env.seq(alice); + auto const bobSeq = env.seq(bob); + env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1), + batch::inner(pay(bob, alice, XRP(5)), bobSeq), + batch::sig(bob), + ter(telINSUF_FEE_P)); + env.close(); + } + + // Bad Fee Dynamic Fee Calculation + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + env.fund(XRP(10000), alice, bob, gw); + env.close(); + auto const ammCreate = + [&alice](STAmount const& amount, STAmount const& amount2) { + Json::Value jv; + jv[jss::Account] = alice.human(); + jv[jss::Amount] = amount.getJson(JsonOptions::none); + jv[jss::Amount2] = amount2.getJson(JsonOptions::none); + jv[jss::TradingFee] = 0; + jv[jss::TransactionType] = jss::AMMCreate; + return jv; + }; + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const seq = env.seq(alice); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(ammCreate(XRP(10), USD(10)), seq + 1), + batch::inner(pay(alice, bob, XRP(10)), seq + 2), + ter(telINSUF_FEE_P)); + env.close(); + } + } + + void + testCalculateBaseFee(FeatureBitset features) + { + testcase("calculate base fee"); + + using namespace test::jtx; + using namespace std::literals; + + // telENV_RPC_FAILED: Batch: txns array exceeds 8 entries. + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + auto const batchFee = batch::calcBatchFee(env, 0, 9); + auto const aliceSeq = env.seq(alice); + env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + ter(telENV_RPC_FAILED)); + env.close(); + } + + // temARRAY_TOO_LARGE: Batch: txns array exceeds 8 entries. + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + auto const batchFee = batch::calcBatchFee(env, 0, 9); + auto const aliceSeq = env.seq(alice); + auto jt = env.jtnofill( + batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq)); + + env.app().openLedger().modify( + [&](OpenView& view, beast::Journal j) { + auto const result = + ripple::apply(env.app(), view, *jt.stx, tapNONE, j); + BEAST_EXPECT( + !result.applied && result.ter == temARRAY_TOO_LARGE); + return result.applied; + }); + } + + // telENV_RPC_FAILED: Batch: signers array exceeds 8 entries. + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + auto const aliceSeq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 9, 2); + env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1), + batch::inner(pay(alice, bob, XRP(5)), aliceSeq + 2), + batch::sig(bob, bob, bob, bob, bob, bob, bob, bob, bob, bob), + ter(telENV_RPC_FAILED)); + env.close(); + } + + // temARRAY_TOO_LARGE: Batch: signers array exceeds 8 entries. + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + auto const batchFee = batch::calcBatchFee(env, 0, 9); + auto const aliceSeq = env.seq(alice); + auto jt = env.jtnofill( + batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1), + batch::inner(pay(alice, bob, XRP(5)), aliceSeq + 2), + batch::sig(bob, bob, bob, bob, bob, bob, bob, bob, bob, bob)); + + env.app().openLedger().modify( + [&](OpenView& view, beast::Journal j) { + auto const result = + ripple::apply(env.app(), view, *jt.stx, tapNONE, j); + BEAST_EXPECT( + !result.applied && result.ter == temARRAY_TOO_LARGE); + return result.applied; + }); + } + } + + void + testAllOrNothing(FeatureBitset features) + { + testcase("all or nothing"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + env.fund(XRP(10000), alice, bob, gw); + env.close(); + + // all + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(2)), seq + 2)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 3); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(3)); + } + + // tec failure + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const seq = env.seq(alice); + + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + // tecUNFUNDED_PAYMENT: alice does not have enough XRP + batch::inner(pay(alice, bob, XRP(9999)), seq + 2)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequence + BEAST_EXPECT(env.seq(alice) == seq + 1); + + // Alice pays Fee; Bob should not be affected + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob); + } + + // tef failure + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + // tefNO_AUTH_REQUIRED: trustline auth is not required + batch::inner(trust(alice, USD(1000), tfSetfAuth), seq + 2)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequence + BEAST_EXPECT(env.seq(alice) == seq + 1); + + // Alice pays Fee; Bob should not be affected + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob); + } + + // ter failure + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + // terPRE_TICKET: ticket does not exist + batch::inner(trust(alice, USD(1000), tfSetfAuth), 0, seq + 2)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequence + BEAST_EXPECT(env.seq(alice) == seq + 1); + + // Alice pays Fee; Bob should not be affected + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob); + } + } + + void + testOnlyOne(FeatureBitset features) + { + testcase("only one"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const dave = Account("dave"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + env.fund(XRP(10000), alice, bob, carol, dave, gw); + env.close(); + + // all transactions fail + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 3); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfOnlyOne), + // tecUNFUNDED_PAYMENT: alice does not have enough XRP + batch::inner(pay(alice, bob, XRP(9999)), seq + 1), + // tecUNFUNDED_PAYMENT: alice does not have enough XRP + batch::inner(pay(alice, bob, XRP(9999)), seq + 2), + // tecUNFUNDED_PAYMENT: alice does not have enough XRP + batch::inner(pay(alice, bob, XRP(9999)), seq + 3)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tecUNFUNDED_PAYMENT", txIDs[0], batchID}, + {2, "Payment", "tecUNFUNDED_PAYMENT", txIDs[1], batchID}, + {3, "Payment", "tecUNFUNDED_PAYMENT", txIDs[2], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 4); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob); + } + + // first transaction fails + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 3); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfOnlyOne), + // tecUNFUNDED_PAYMENT: alice does not have enough XRP + batch::inner(pay(alice, bob, XRP(9999)), seq + 1), + batch::inner(pay(alice, bob, XRP(1)), seq + 2), + batch::inner(pay(alice, bob, XRP(2)), seq + 3)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tecUNFUNDED_PAYMENT", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 3); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(1)); + } + + // tec failure + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 3); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfOnlyOne), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + // tecUNFUNDED_PAYMENT: alice does not have enough XRP + batch::inner(pay(alice, bob, XRP(9999)), seq + 2), + batch::inner(pay(alice, bob, XRP(2)), seq + 3)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 2); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(1)); + } + + // tef failure + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 3); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfOnlyOne), + // tefNO_AUTH_REQUIRED: trustline auth is not required + batch::inner(trust(alice, USD(1000), tfSetfAuth), seq + 1), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(2)), seq + 3)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 2); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee - XRP(1)); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(1)); + } + + // ter failure + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 3); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfOnlyOne), + // terPRE_TICKET: ticket does not exist + batch::inner(trust(alice, USD(1000), tfSetfAuth), 0, seq + 1), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(2)), seq + 3)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 2); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee - XRP(1)); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(1)); + } + + // tec (tecKILLED) error + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + auto const preCarol = env.balance(carol); + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 6); + + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfOnlyOne), + batch::inner( + offer( + alice, + alice["USD"](100), + XRP(100), + tfImmediateOrCancel), + seq + 1), + batch::inner( + offer( + alice, + alice["USD"](100), + XRP(100), + tfImmediateOrCancel), + seq + 2), + batch::inner( + offer( + alice, + alice["USD"](100), + XRP(100), + tfImmediateOrCancel), + seq + 3), + batch::inner(pay(alice, bob, XRP(100)), seq + 4), + batch::inner(pay(alice, carol, XRP(100)), seq + 5), + batch::inner(pay(alice, dave, XRP(100)), seq + 6)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "OfferCreate", "tecKILLED", txIDs[0], batchID}, + {2, "OfferCreate", "tecKILLED", txIDs[1], batchID}, + {3, "OfferCreate", "tecKILLED", txIDs[2], batchID}, + {4, "Payment", "tesSUCCESS", txIDs[3], batchID}, + }; + validateClosedLedger(env, testCases); + + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(100) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(100)); + BEAST_EXPECT(env.balance(carol) == preCarol); + } + } + + void + testUntilFailure(FeatureBitset features) + { + testcase("until failure"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const dave = Account("dave"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + env.fund(XRP(10000), alice, bob, carol, dave, gw); + env.close(); + + // first transaction fails + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 4); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfUntilFailure), + // tecUNFUNDED_PAYMENT: alice does not have enough XRP + batch::inner(pay(alice, bob, XRP(9999)), seq + 1), + batch::inner(pay(alice, bob, XRP(1)), seq + 2), + batch::inner(pay(alice, bob, XRP(2)), seq + 3), + batch::inner(pay(alice, bob, XRP(3)), seq + 4)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tecUNFUNDED_PAYMENT", txIDs[0], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 2); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob); + } + + // all transactions succeed + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 4); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfUntilFailure), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(2)), seq + 2), + batch::inner(pay(alice, bob, XRP(3)), seq + 3), + batch::inner(pay(alice, bob, XRP(4)), seq + 4)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + {3, "Payment", "tesSUCCESS", txIDs[2], batchID}, + {4, "Payment", "tesSUCCESS", txIDs[3], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 5); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(10) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(10)); + } + + // tec error + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 4); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfUntilFailure), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(2)), seq + 2), + // tecUNFUNDED_PAYMENT: alice does not have enough XRP + batch::inner(pay(alice, bob, XRP(9999)), seq + 3), + batch::inner(pay(alice, bob, XRP(3)), seq + 4)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + {3, "Payment", "tecUNFUNDED_PAYMENT", txIDs[2], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 4); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(3)); + } + + // tef error + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 4); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfUntilFailure), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(2)), seq + 2), + // tefNO_AUTH_REQUIRED: trustline auth is not required + batch::inner(trust(alice, USD(1000), tfSetfAuth), seq + 3), + batch::inner(pay(alice, bob, XRP(3)), seq + 4)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 3); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(3)); + } + + // ter error + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 4); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfUntilFailure), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(2)), seq + 2), + // terPRE_TICKET: ticket does not exist + batch::inner(trust(alice, USD(1000), tfSetfAuth), 0, seq + 3), + batch::inner(pay(alice, bob, XRP(3)), seq + 4)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 3); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(3)); + } + + // tec (tecKILLED) error + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + auto const preCarol = env.balance(carol); + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 4); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfUntilFailure), + batch::inner(pay(alice, bob, XRP(100)), seq + 1), + batch::inner(pay(alice, carol, XRP(100)), seq + 2), + batch::inner( + offer( + alice, + alice["USD"](100), + XRP(100), + tfImmediateOrCancel), + seq + 3), + batch::inner(pay(alice, dave, XRP(100)), seq + 4)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + {3, "OfferCreate", "tecKILLED", txIDs[2], batchID}, + }; + validateClosedLedger(env, testCases); + + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(200) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(100)); + BEAST_EXPECT(env.balance(carol) == preCarol + XRP(100)); + } + } + + void + testIndependent(FeatureBitset features) + { + testcase("independent"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + env.fund(XRP(10000), alice, bob, carol, gw); + env.close(); + + // multiple transactions fail + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 4); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfIndependent), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + // tecUNFUNDED_PAYMENT: alice does not have enough XRP + batch::inner(pay(alice, bob, XRP(9999)), seq + 2), + // tecUNFUNDED_PAYMENT: alice does not have enough XRP + batch::inner(pay(alice, bob, XRP(9999)), seq + 3), + batch::inner(pay(alice, bob, XRP(3)), seq + 4)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tecUNFUNDED_PAYMENT", txIDs[1], batchID}, + {3, "Payment", "tecUNFUNDED_PAYMENT", txIDs[2], batchID}, + {4, "Payment", "tesSUCCESS", txIDs[3], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 5); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(4) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(4)); + } + + // tec error + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 4); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfIndependent), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(2)), seq + 2), + // tecUNFUNDED_PAYMENT: alice does not have enough XRP + batch::inner(pay(alice, bob, XRP(9999)), seq + 3), + batch::inner(pay(alice, bob, XRP(3)), seq + 4)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + {3, "Payment", "tecUNFUNDED_PAYMENT", txIDs[2], batchID}, + {4, "Payment", "tesSUCCESS", txIDs[3], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 5); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(6) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(6)); + } + + // tef error + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 4); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfIndependent), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(2)), seq + 2), + // tefNO_AUTH_REQUIRED: trustline auth is not required + batch::inner(trust(alice, USD(1000), tfSetfAuth), seq + 3), + batch::inner(pay(alice, bob, XRP(3)), seq + 3)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + {3, "Payment", "tesSUCCESS", txIDs[3], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 4); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee - XRP(6)); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(6)); + } + + // ter error + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 4); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfIndependent), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(2)), seq + 2), + // terPRE_TICKET: ticket does not exist + batch::inner(trust(alice, USD(1000), tfSetfAuth), 0, seq + 3), + batch::inner(pay(alice, bob, XRP(3)), seq + 3)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + {3, "Payment", "tesSUCCESS", txIDs[3], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 4); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee - XRP(6)); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(6)); + } + + // tec (tecKILLED) error + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + auto const preCarol = env.balance(carol); + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 3); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfIndependent), + batch::inner(pay(alice, bob, XRP(100)), seq + 1), + batch::inner(pay(alice, carol, XRP(100)), seq + 2), + batch::inner( + offer( + alice, + alice["USD"](100), + XRP(100), + tfImmediateOrCancel), + seq + 3)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + {3, "OfferCreate", "tecKILLED", txIDs[2], batchID}, + }; + validateClosedLedger(env, testCases); + + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(200) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(100)); + BEAST_EXPECT(env.balance(carol) == preCarol + XRP(100)); + } + } + + void + testInnerSubmitRPC(FeatureBitset features) + { + testcase("inner submit rpc"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + env.fund(XRP(10000), alice, bob); + env.close(); + + auto submitAndValidate = [&](Slice const& slice) { + auto const jrr = env.rpc("submit", strHex(slice))[jss::result]; + BEAST_EXPECT( + jrr[jss::status] == "error" && + jrr[jss::error] == "invalidTransaction" && + jrr[jss::error_exception] == + "fails local checks: Malformed: Invalid inner batch " + "transaction."); + env.close(); + }; + + // Invalid RPC Submission: TxnSignature + // - has `TxnSignature` field + // - has no `SigningPubKey` field + // - has no `Signers` field + // - has `tfInnerBatchTxn` flag + { + auto txn = batch::inner(pay(alice, bob, XRP(1)), env.seq(alice)); + txn[sfTxnSignature] = "DEADBEEF"; + STParsedJSONObject parsed("test", txn.getTxn()); + Serializer s; + parsed.object->add(s); + submitAndValidate(s.slice()); + } + + // Invalid RPC Submission: SigningPubKey + // - has no `TxnSignature` field + // - has `SigningPubKey` field + // - has no `Signers` field + // - has `tfInnerBatchTxn` flag + { + auto txn = batch::inner(pay(alice, bob, XRP(1)), env.seq(alice)); + txn[sfSigningPubKey] = strHex(alice.pk()); + STParsedJSONObject parsed("test", txn.getTxn()); + Serializer s; + parsed.object->add(s); + submitAndValidate(s.slice()); + } + + // Invalid RPC Submission: Signers + // - has no `TxnSignature` field + // - has empty `SigningPubKey` field + // - has `Signers` field + // - has `tfInnerBatchTxn` flag + { + auto txn = batch::inner(pay(alice, bob, XRP(1)), env.seq(alice)); + txn[sfSigners] = Json::arrayValue; + STParsedJSONObject parsed("test", txn.getTxn()); + Serializer s; + parsed.object->add(s); + submitAndValidate(s.slice()); + } + + // Invalid RPC Submission: tfInnerBatchTxn + // - has no `TxnSignature` field + // - has empty `SigningPubKey` field + // - has no `Signers` field + // - has `tfInnerBatchTxn` flag + { + auto txn = batch::inner(pay(alice, bob, XRP(1)), env.seq(alice)); + STParsedJSONObject parsed("test", txn.getTxn()); + Serializer s; + parsed.object->add(s); + auto const jrr = env.rpc("submit", strHex(s.slice()))[jss::result]; + BEAST_EXPECT( + jrr[jss::status] == "success" && + jrr[jss::engine_result] == "temINVALID_FLAG"); + + env.close(); + } + } + + void + testAccountActivation(FeatureBitset features) + { + testcase("account activation"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice); + env.close(); + env.memoize(bob); + + auto const preAlice = env.balance(alice); + auto const ledSeq = env.current()->seq(); + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1000)), seq + 1), + batch::inner(fset(bob, asfAllowTrustLineClawback), ledSeq), + batch::sig(bob)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "AccountSet", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 2); + + // Bob consumes sequences (# of txns) + BEAST_EXPECT(env.seq(bob) == ledSeq + 1); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1000) - batchFee); + BEAST_EXPECT(env.balance(bob) == XRP(1000)); + } + + void + testAccountSet(FeatureBitset features) + { + testcase("account set"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto tx1 = batch::inner(noop(alice), seq + 1); + std::string domain = "example.com"; + tx1[sfDomain] = strHex(domain); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfAllOrNothing), + tx1, + batch::inner(pay(alice, bob, XRP(1)), seq + 2)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "AccountSet", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + auto const sle = env.le(keylet::account(alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT( + sle->getFieldVL(sfDomain) == Blob(domain.begin(), domain.end())); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 3); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(1)); + } + + void + testAccountDelete(FeatureBitset features) + { + testcase("account delete"); + + using namespace test::jtx; + using namespace std::literals; + + // tfIndependent: account delete success + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + incLgrSeqForAccDel(env, alice); + for (int i = 0; i < 5; ++i) + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2) + + env.current()->fees().increment; + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfIndependent), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(acctdelete(alice, bob), seq + 2), + // terNO_ACCOUNT: alice does not exist + batch::inner(pay(alice, bob, XRP(2)), seq + 3)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "AccountDelete", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice does not exist; Bob receives Alice's XRP + BEAST_EXPECT(!env.le(keylet::account(alice))); + BEAST_EXPECT(env.balance(bob) == preBob + (preAlice - batchFee)); + } + + // tfIndependent: account delete fails + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + incLgrSeqForAccDel(env, alice); + for (int i = 0; i < 5; ++i) + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + env.trust(bob["USD"](1000), alice); + env.close(); + + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2) + + env.current()->fees().increment; + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfIndependent), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + // tecHAS_OBLIGATIONS: alice has obligations + batch::inner(acctdelete(alice, bob), seq + 2), + batch::inner(pay(alice, bob, XRP(2)), seq + 3)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "AccountDelete", "tecHAS_OBLIGATIONS", txIDs[1], batchID}, + {3, "Payment", "tesSUCCESS", txIDs[2], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice does not exist; Bob receives XRP + BEAST_EXPECT(env.le(keylet::account(alice))); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(3)); + } + + // tfAllOrNothing: account delete fails + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + incLgrSeqForAccDel(env, alice); + for (int i = 0; i < 5; ++i) + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2) + + env.current()->fees().increment; + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(acctdelete(alice, bob), seq + 2), + // terNO_ACCOUNT: alice does not exist + batch::inner(pay(alice, bob, XRP(2)), seq + 3)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + }; + validateClosedLedger(env, testCases); + + // Alice still exists; Bob is unchanged + BEAST_EXPECT(env.le(keylet::account(alice))); + BEAST_EXPECT(env.balance(bob) == preBob); + } + } + + void + testObjectCreateSequence(FeatureBitset features) + { + testcase("object create w/ sequence"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + env.fund(XRP(10000), alice, bob, gw); + env.close(); + + env.trust(USD(1000), alice, bob); + env(pay(gw, alice, USD(100))); + env(pay(gw, bob, USD(100))); + env.close(); + + // success + { + auto const aliceSeq = env.seq(alice); + auto const bobSeq = env.seq(bob); + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBobUSD = env.balance(bob, USD.issue()); + + auto const batchFee = batch::calcBatchFee(env, 1, 2); + uint256 const chkID{getCheckIndex(bob, env.seq(bob))}; + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(check::create(bob, alice, USD(10)), bobSeq), + batch::inner(check::cash(alice, chkID, USD(10)), aliceSeq + 1), + batch::sig(bob)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "CheckCreate", "tesSUCCESS", txIDs[0], batchID}, + {2, "CheckCash", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == aliceSeq + 2); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(bob) == bobSeq + 1); + + // Alice pays Fee; Bob XRP Unchanged + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob); + + // Alice pays USD & Bob receives USD + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAliceUSD + USD(10)); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD - USD(10)); + } + + // failure + { + env(fset(alice, asfRequireDest)); + env.close(); + + auto const aliceSeq = env.seq(alice); + auto const bobSeq = env.seq(bob); + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBobUSD = env.balance(bob, USD.issue()); + + auto const batchFee = batch::calcBatchFee(env, 1, 2); + uint256 const chkID{getCheckIndex(bob, env.seq(bob))}; + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, aliceSeq, batchFee, tfIndependent), + // tecDST_TAG_NEEDED - alice has enabled asfRequireDest + batch::inner(check::create(bob, alice, USD(10)), bobSeq), + batch::inner(check::cash(alice, chkID, USD(10)), aliceSeq + 1), + batch::sig(bob)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "CheckCreate", "tecDST_TAG_NEEDED", txIDs[0], batchID}, + {2, "CheckCash", "tecNO_ENTRY", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == aliceSeq + 2); + + // Bob consumes sequences (# of txns) + BEAST_EXPECT(env.seq(bob) == bobSeq + 1); + + // Alice pays Fee; Bob XRP Unchanged + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob); + + // Alice pays USD & Bob receives USD + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD); + } + } + + void + testObjectCreateTicket(FeatureBitset features) + { + testcase("object create w/ ticket"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + env.fund(XRP(10000), alice, bob, gw); + env.close(); + + env.trust(USD(1000), alice, bob); + env(pay(gw, alice, USD(100))); + env(pay(gw, bob, USD(100))); + env.close(); + + auto const aliceSeq = env.seq(alice); + auto const bobSeq = env.seq(bob); + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBobUSD = env.balance(bob, USD.issue()); + + auto const batchFee = batch::calcBatchFee(env, 1, 3); + uint256 const chkID{getCheckIndex(bob, bobSeq + 1)}; + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(ticket::create(bob, 10), bobSeq), + batch::inner(check::create(bob, alice, USD(10)), 0, bobSeq + 1), + batch::inner(check::cash(alice, chkID, USD(10)), aliceSeq + 1), + batch::sig(bob)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "TicketCreate", "tesSUCCESS", txIDs[0], batchID}, + {2, "CheckCreate", "tesSUCCESS", txIDs[1], batchID}, + {3, "CheckCash", "tesSUCCESS", txIDs[2], batchID}, + }; + validateClosedLedger(env, testCases); + + BEAST_EXPECT(env.seq(alice) == aliceSeq + 2); + BEAST_EXPECT(env.seq(bob) == bobSeq + 10 + 1); + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob); + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD + USD(10)); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD - USD(10)); + } + + void + testObjectCreate3rdParty(FeatureBitset features) + { + testcase("object create w/ 3rd party"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + env.fund(XRP(10000), alice, bob, carol, gw); + env.close(); + + env.trust(USD(1000), alice, bob); + env(pay(gw, alice, USD(100))); + env(pay(gw, bob, USD(100))); + env.close(); + + auto const aliceSeq = env.seq(alice); + auto const bobSeq = env.seq(bob); + auto const carolSeq = env.seq(carol); + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + auto const preCarol = env.balance(carol); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBobUSD = env.balance(bob, USD.issue()); + + auto const batchFee = batch::calcBatchFee(env, 2, 2); + uint256 const chkID{getCheckIndex(bob, env.seq(bob))}; + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(carol, carolSeq, batchFee, tfAllOrNothing), + batch::inner(check::create(bob, alice, USD(10)), bobSeq), + batch::inner(check::cash(alice, chkID, USD(10)), aliceSeq), + batch::sig(alice, bob)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "CheckCreate", "tesSUCCESS", txIDs[0], batchID}, + {2, "CheckCash", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); + BEAST_EXPECT(env.seq(bob) == bobSeq + 1); + BEAST_EXPECT(env.seq(carol) == carolSeq + 1); + BEAST_EXPECT(env.balance(alice) == preAlice); + BEAST_EXPECT(env.balance(bob) == preBob); + BEAST_EXPECT(env.balance(carol) == preCarol - batchFee); + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD + USD(10)); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD - USD(10)); + } + + void + testTickets(FeatureBitset features) + { + { + testcase("tickets outer"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + env.fund(XRP(10000), alice, bob); + env.close(); + + std::uint32_t aliceTicketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 10)); + env.close(); + + auto const aliceSeq = env.seq(alice); + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, 0, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq + 0), + batch::inner(pay(alice, bob, XRP(2)), aliceSeq + 1), + ticket::use(aliceTicketSeq)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + auto const sle = env.le(keylet::account(alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->getFieldU32(sfOwnerCount) == 9); + BEAST_EXPECT(sle->getFieldU32(sfTicketCount) == 9); + + BEAST_EXPECT(env.seq(alice) == aliceSeq + 2); + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(3)); + } + + { + testcase("tickets inner"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + env.fund(XRP(10000), alice, bob); + env.close(); + + std::uint32_t aliceTicketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 10)); + env.close(); + + auto const aliceSeq = env.seq(alice); + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq), + batch::inner(pay(alice, bob, XRP(2)), 0, aliceTicketSeq + 1)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + auto const sle = env.le(keylet::account(alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->getFieldU32(sfOwnerCount) == 8); + BEAST_EXPECT(sle->getFieldU32(sfTicketCount) == 8); + + BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(3)); + } + + { + testcase("tickets outer inner"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + env.fund(XRP(10000), alice, bob); + env.close(); + + std::uint32_t aliceTicketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 10)); + env.close(); + + auto const aliceSeq = env.seq(alice); + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, 0, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1), + batch::inner(pay(alice, bob, XRP(2)), aliceSeq), + ticket::use(aliceTicketSeq)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + auto const sle = env.le(keylet::account(alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->getFieldU32(sfOwnerCount) == 8); + BEAST_EXPECT(sle->getFieldU32(sfTicketCount) == 8); + + BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(3)); + } + } + + void + testSequenceOpenLedger(FeatureBitset features) + { + testcase("sequence open ledger"); + + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + + // Before Batch Txn w/ retry following ledger + { + // IMPORTANT: The batch txn is applied first, then the noop txn. + // Because of this ordering, the noop txn is not applied and is + // overwritten by the payment in the batch transaction. Because the + // terPRE_SEQ is outside of the batch this noop transaction will ge + // reapplied in the following ledger + test::jtx::Env env{*this, envconfig()}; + env.fund(XRP(10000), alice, bob, carol); + env.close(); + + auto const aliceSeq = env.seq(alice); + auto const carolSeq = env.seq(carol); + + // AccountSet Txn + auto const noopTxn = env.jt(noop(alice), seq(aliceSeq + 2)); + auto const noopTxnID = to_string(noopTxn.stx->getTransactionID()); + env(noopTxn, ter(terPRE_SEQ)); + + // Batch Txn + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(carol, carolSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(2)), aliceSeq + 1), + batch::sig(alice)); + env.close(); + + { + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + } + + env.close(); + { + // next ledger contains noop txn + std::vector testCases = { + {0, "AccountSet", "tesSUCCESS", noopTxnID, std::nullopt}, + }; + validateClosedLedger(env, testCases); + } + } + + // Before Batch Txn w/ same sequence + { + // IMPORTANT: The batch txn is applied first, then the noop txn. + // Because of this ordering, the noop txn is not applied and is + // overwritten by the payment in the batch transaction. + test::jtx::Env env{*this, envconfig()}; + env.fund(XRP(10000), alice, bob); + env.close(); + + auto const aliceSeq = env.seq(alice); + + // AccountSet Txn + auto const noopTxn = env.jt(noop(alice), seq(aliceSeq + 1)); + env(noopTxn, ter(terPRE_SEQ)); + + // Batch Txn + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq + 1), + batch::inner(pay(alice, bob, XRP(2)), aliceSeq + 2)); + env.close(); + + { + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + } + + env.close(); + { + // next ledger is empty + std::vector testCases = {}; + validateClosedLedger(env, testCases); + } + } + + // After Batch Txn w/ same sequence + { + // IMPORTANT: The batch txn is applied first, then the noop txn. + // Because of this ordering, the noop txn is not applied and is + // overwritten by the payment in the batch transaction. + test::jtx::Env env{*this, envconfig()}; + env.fund(XRP(10000), alice, bob); + env.close(); + + auto const aliceSeq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq + 1), + batch::inner(pay(alice, bob, XRP(2)), aliceSeq + 2)); + + auto const noopTxn = env.jt(noop(alice), seq(aliceSeq + 1)); + auto const noopTxnID = to_string(noopTxn.stx->getTransactionID()); + env(noopTxn, ter(tesSUCCESS)); + env.close(); + + { + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + } + + env.close(); + { + // next ledger is empty + std::vector testCases = {}; + validateClosedLedger(env, testCases); + } + } + + // Outer Batch terPRE_SEQ + { + test::jtx::Env env{*this, envconfig()}; + env.fund(XRP(10000), alice, bob, carol); + env.close(); + + auto const aliceSeq = env.seq(alice); + auto const carolSeq = env.seq(carol); + + // Batch Txn + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const [txIDs, batchID] = submitBatch( + env, + terPRE_SEQ, + batch::outer(carol, carolSeq + 1, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(2)), aliceSeq + 1), + batch::sig(alice)); + + // AccountSet Txn + auto const noopTxn = env.jt(noop(carol), seq(carolSeq)); + auto const noopTxnID = to_string(noopTxn.stx->getTransactionID()); + env(noopTxn, ter(tesSUCCESS)); + env.close(); + + { + std::vector testCases = { + {0, "AccountSet", "tesSUCCESS", noopTxnID, std::nullopt}, + {1, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {2, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {3, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + } + + env.close(); + { + // next ledger contains no transactions + std::vector testCases = {}; + validateClosedLedger(env, testCases); + } + } + } + + void + testTicketsOpenLedger(FeatureBitset features) + { + testcase("tickets open ledger"); + + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + // Before Batch Txn w/ same ticket + { + // IMPORTANT: The batch txn is applied first, then the noop txn. + // Because of this ordering, the noop txn is not applied and is + // overwritten by the payment in the batch transaction. + test::jtx::Env env{*this, envconfig()}; + env.fund(XRP(10000), alice, bob); + env.close(); + + std::uint32_t aliceTicketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 10)); + env.close(); + + auto const aliceSeq = env.seq(alice); + + // AccountSet Txn + auto const noopTxn = + env.jt(noop(alice), ticket::use(aliceTicketSeq + 1)); + auto const noopTxnID = to_string(noopTxn.stx->getTransactionID()); + env(noopTxn, ter(tesSUCCESS)); + + // Batch Txn + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, 0, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1), + batch::inner(pay(alice, bob, XRP(2)), aliceSeq), + ticket::use(aliceTicketSeq)); + env.close(); + + { + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + } + + env.close(); + { + // next ledger is empty + std::vector testCases = {}; + validateClosedLedger(env, testCases); + } + } + + // After Batch Txn w/ same ticket + { + // IMPORTANT: The batch txn is applied first, then the noop txn. + // Because of this ordering, the noop txn is not applied and is + // overwritten by the payment in the batch transaction. + test::jtx::Env env{*this, envconfig()}; + env.fund(XRP(10000), alice, bob); + env.close(); + + std::uint32_t aliceTicketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 10)); + env.close(); + + auto const aliceSeq = env.seq(alice); + + // Batch Txn + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, 0, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1), + batch::inner(pay(alice, bob, XRP(2)), aliceSeq), + ticket::use(aliceTicketSeq)); + + // AccountSet Txn + auto const noopTxn = + env.jt(noop(alice), ticket::use(aliceTicketSeq + 1)); + env(noopTxn); + + env.close(); + { + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + } + + env.close(); + { + // next ledger is empty + std::vector testCases = {}; + validateClosedLedger(env, testCases); + } + } + } + + void + testObjectsOpenLedger(FeatureBitset features) + { + testcase("objects open ledger"); + + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + // Consume Object Before Batch Txn + { + // IMPORTANT: The initial result of `CheckCash` is tecNO_ENTRY + // because the create transaction has not been applied because the + // batch will run in the close ledger process. The batch will be + // allied and then retry this transaction in the current ledger. + + test::jtx::Env env{*this, envconfig()}; + env.fund(XRP(10000), alice, bob); + env.close(); + + std::uint32_t aliceTicketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 10)); + env.close(); + + auto const aliceSeq = env.seq(alice); + + // CheckCash Txn + uint256 const chkID{getCheckIndex(alice, aliceSeq)}; + auto const objTxn = env.jt(check::cash(bob, chkID, XRP(10))); + auto const objTxnID = to_string(objTxn.stx->getTransactionID()); + env(objTxn, ter(tecNO_ENTRY)); + + // Batch Txn + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, 0, batchFee, tfAllOrNothing), + batch::inner(check::create(alice, bob, XRP(10)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1), + ticket::use(aliceTicketSeq)); + + env.close(); + { + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "CheckCreate", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + {3, "CheckCash", "tesSUCCESS", objTxnID, std::nullopt}, + }; + validateClosedLedger(env, testCases); + } + + env.close(); + { + // next ledger is empty + std::vector testCases = {}; + validateClosedLedger(env, testCases); + } + } + + // Create Object Before Batch Txn + { + test::jtx::Env env{*this, envconfig()}; + env.fund(XRP(10000), alice, bob); + env.close(); + + std::uint32_t aliceTicketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 10)); + env.close(); + + auto const aliceSeq = env.seq(alice); + auto const bobSeq = env.seq(bob); + + // CheckCreate Txn + uint256 const chkID{getCheckIndex(alice, aliceSeq)}; + auto const objTxn = env.jt(check::create(alice, bob, XRP(10))); + auto const objTxnID = to_string(objTxn.stx->getTransactionID()); + env(objTxn, ter(tesSUCCESS)); + + // Batch Txn + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, 0, batchFee, tfAllOrNothing), + batch::inner(check::cash(bob, chkID, XRP(10)), bobSeq), + batch::inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1), + ticket::use(aliceTicketSeq), + batch::sig(bob)); + + env.close(); + { + std::vector testCases = { + {0, "CheckCreate", "tesSUCCESS", objTxnID, std::nullopt}, + {1, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {2, "CheckCash", "tesSUCCESS", txIDs[0], batchID}, + {3, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + } + } + + // After Batch Txn + { + // IMPORTANT: The initial result of `CheckCash` is tecNO_ENTRY + // because the create transaction has not been applied because the + // batch will run in the close ledger process. The batch will be + // applied and then retry this transaction in the current ledger. + + test::jtx::Env env{*this, envconfig()}; + env.fund(XRP(10000), alice, bob); + env.close(); + + std::uint32_t aliceTicketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 10)); + env.close(); + + auto const aliceSeq = env.seq(alice); + + // Batch Txn + auto const batchFee = batch::calcBatchFee(env, 0, 2); + uint256 const chkID{getCheckIndex(alice, aliceSeq)}; + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, 0, batchFee, tfAllOrNothing), + batch::inner(check::create(alice, bob, XRP(10)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1), + ticket::use(aliceTicketSeq)); + + // CheckCash Txn + auto const objTxn = env.jt(check::cash(bob, chkID, XRP(10))); + auto const objTxnID = to_string(objTxn.stx->getTransactionID()); + env(objTxn, ter(tecNO_ENTRY)); + + env.close(); + { + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "CheckCreate", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + {3, "CheckCash", "tesSUCCESS", objTxnID, std::nullopt}, + }; + validateClosedLedger(env, testCases); + } + } + } + + void + testPseudoTxn(FeatureBitset features) + { + testcase("pseudo txn with tfInnerBatchTxn"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + STTx const stx = STTx(ttAMENDMENT, [&](auto& obj) { + obj.setAccountID(sfAccount, AccountID()); + obj.setFieldH256(sfAmendment, uint256(2)); + obj.setFieldU32(sfLedgerSequence, env.seq(alice)); + obj.setFieldU32(sfFlags, tfInnerBatchTxn); + }); + + std::string reason; + BEAST_EXPECT(isPseudoTx(stx)); + BEAST_EXPECT(!passesLocalChecks(stx, reason)); + BEAST_EXPECT(reason == "Cannot submit pseudo transactions."); + env.app().openLedger().modify([&](OpenView& view, beast::Journal j) { + auto const result = ripple::apply(env.app(), view, stx, tapNONE, j); + BEAST_EXPECT(!result.applied && result.ter == temINVALID_FLAG); + return result.applied; + }); + } + + void + testOpenLedger(FeatureBitset features) + { + testcase("batch open ledger"); + // IMPORTANT: When a transaction is submitted outside of a batch and + // another transaction is part of the batch, the batch might fail + // because the sequence is out of order. This is because the canonical + // order of transactions is determined by the account first. So in this + // case, alice's batch comes after bobs self submitted transaction even + // though the payment was submitted after the batch. + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + XRPAmount const baseFee = env.current()->fees().base; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + env.fund(XRP(10000), alice, bob); + env.close(); + + env(noop(bob), ter(tesSUCCESS)); + env.close(); + + auto const aliceSeq = env.seq(alice); + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + auto const bobSeq = env.seq(bob); + + // Alice Pays Bob (Open Ledger) + auto const payTxn1 = env.jt(pay(alice, bob, XRP(10)), seq(aliceSeq)); + auto const payTxn1ID = to_string(payTxn1.stx->getTransactionID()); + env(payTxn1, ter(tesSUCCESS)); + + // Alice & Bob Atomic Batch + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, aliceSeq + 1, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 2), + batch::inner(pay(bob, alice, XRP(5)), bobSeq), + batch::sig(bob)); + + // Bob pays Alice (Open Ledger) + auto const payTxn2 = env.jt(pay(bob, alice, XRP(5)), seq(bobSeq + 1)); + auto const payTxn2ID = to_string(payTxn2.stx->getTransactionID()); + env(payTxn2, ter(terPRE_SEQ)); + env.close(); + + std::vector testCases = { + {0, "Payment", "tesSUCCESS", payTxn1ID, std::nullopt}, + {1, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {2, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {3, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + env.close(); + { + // next ledger includes the payment txn + std::vector testCases = { + {0, "Payment", "tesSUCCESS", payTxn2ID, std::nullopt}, + }; + validateClosedLedger(env, testCases); + } + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == aliceSeq + 3); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(bob) == bobSeq + 2); + + // Alice pays XRP & Fee; Bob receives XRP & pays Fee + BEAST_EXPECT( + env.balance(alice) == preAlice - XRP(10) - batchFee - baseFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(10) - baseFee); + } + + void + testBatchTxQueue(FeatureBitset features) + { + testcase("batch tx queue"); + + using namespace test::jtx; + using namespace std::literals; + + // only outer batch transactions are counter towards the queue size + { + test::jtx::Env env{ + *this, + makeSmallQueueConfig( + {{"minimum_txn_in_ledger_standalone", "2"}}), + nullptr, + beast::severities::kError}; + + auto alice = Account("alice"); + auto bob = Account("bob"); + auto carol = Account("carol"); + + // Fund across several ledgers so the TxQ metrics stay restricted. + env.fund(XRP(10000), noripple(alice, bob)); + env.close(env.now() + 5s, 10000ms); + env.fund(XRP(10000), noripple(carol)); + env.close(env.now() + 5s, 10000ms); + + // Fill the ledger + env(noop(alice)); + env(noop(alice)); + env(noop(alice)); + checkMetrics(*this, env, 0, std::nullopt, 3, 2); + + env(noop(carol), ter(terQUEUED)); + checkMetrics(*this, env, 1, std::nullopt, 3, 2); + + auto const aliceSeq = env.seq(alice); + auto const bobSeq = env.seq(bob); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + + // Queue Batch + { + env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1), + batch::inner(pay(bob, alice, XRP(5)), bobSeq), + batch::sig(bob), + ter(terQUEUED)); + } + + checkMetrics(*this, env, 2, std::nullopt, 3, 2); + + // Replace Queued Batch + { + env(batch::outer( + alice, + aliceSeq, + openLedgerFee(env, batchFee), + tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1), + batch::inner(pay(bob, alice, XRP(5)), bobSeq), + batch::sig(bob), + ter(tesSUCCESS)); + env.close(); + } + + checkMetrics(*this, env, 0, 12, 1, 6); + } + + // inner batch transactions are counter towards the ledger tx count + { + test::jtx::Env env{ + *this, + makeSmallQueueConfig( + {{"minimum_txn_in_ledger_standalone", "2"}}), + nullptr, + beast::severities::kError}; + + auto alice = Account("alice"); + auto bob = Account("bob"); + auto carol = Account("carol"); + + // Fund across several ledgers so the TxQ metrics stay restricted. + env.fund(XRP(10000), noripple(alice, bob)); + env.close(env.now() + 5s, 10000ms); + env.fund(XRP(10000), noripple(carol)); + env.close(env.now() + 5s, 10000ms); + + // Fill the ledger leaving room for 1 queued transaction + env(noop(alice)); + env(noop(alice)); + checkMetrics(*this, env, 0, std::nullopt, 2, 2); + + auto const aliceSeq = env.seq(alice); + auto const bobSeq = env.seq(bob); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + + // Batch Successful + { + env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1), + batch::inner(pay(bob, alice, XRP(5)), bobSeq), + batch::sig(bob), + ter(tesSUCCESS)); + } + + checkMetrics(*this, env, 0, std::nullopt, 3, 2); + + env(noop(carol), ter(terQUEUED)); + checkMetrics(*this, env, 1, std::nullopt, 3, 2); + } + } + + void + testBatchNetworkOps(FeatureBitset features) + { + testcase("batch network ops"); + + using namespace test::jtx; + using namespace std::literals; + + Env env( + *this, + envconfig(), + features, + nullptr, + beast::severities::kDisabled); + + auto alice = Account("alice"); + auto bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + auto submitTx = [&](std::uint32_t flags) -> uint256 { + auto jt = env.jt(pay(alice, bob, XRP(1)), txflags(flags)); + Serializer s; + jt.stx->add(s); + env.app().getOPs().submitTransaction(jt.stx); + return jt.stx->getTransactionID(); + }; + + auto processTxn = [&](std::uint32_t flags) -> uint256 { + auto jt = env.jt(pay(alice, bob, XRP(1)), txflags(flags)); + Serializer s; + jt.stx->add(s); + std::string reason; + auto transaction = + std::make_shared(jt.stx, reason, env.app()); + env.app().getOPs().processTransaction( + transaction, false, true, NetworkOPs::FailHard::yes); + return transaction->getID(); + }; + + // Validate: NetworkOPs::submitTransaction() + { + // Submit a tx with tfInnerBatchTxn + uint256 const txBad = submitTx(tfInnerBatchTxn); + BEAST_EXPECT(env.app().getHashRouter().getFlags(txBad) == 0); + } + + // Validate: NetworkOPs::processTransaction() + { + uint256 const txid = processTxn(tfInnerBatchTxn); + // HashRouter::getFlags() should return SF_BAD + BEAST_EXPECT(env.app().getHashRouter().getFlags(txid) == SF_BAD); + } + } + + void + testBatchDelegate(FeatureBitset features) + { + testcase("batch delegate"); + + using namespace test::jtx; + using namespace std::literals; + + // delegated non atomic inner + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + env.fund(XRP(10000), alice, bob, gw); + env.close(); + + env(delegate::set(alice, bob, {"Payment"})); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const seq = env.seq(alice); + + auto tx = batch::inner(pay(alice, bob, XRP(1)), seq + 1); + tx[jss::Delegate] = bob.human(); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfAllOrNothing), + tx, + batch::inner(pay(alice, bob, XRP(2)), seq + 2)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 3); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(3)); + } + + // delegated atomic inner + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + env.fund(XRP(10000), alice, bob, carol, gw); + env.close(); + + env(delegate::set(bob, carol, {"Payment"})); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + auto const preCarol = env.balance(carol); + + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const aliceSeq = env.seq(alice); + auto const bobSeq = env.seq(bob); + + auto tx = batch::inner(pay(bob, alice, XRP(1)), bobSeq); + tx[jss::Delegate] = carol.human(); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + tx, + batch::inner(pay(alice, bob, XRP(2)), aliceSeq + 1), + batch::sig(bob)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + BEAST_EXPECT(env.seq(alice) == aliceSeq + 2); + BEAST_EXPECT(env.seq(bob) == bobSeq + 1); + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(1)); + // NOTE: Carol would normally pay the fee for delegated txns, but + // because the batch is atomic, the fee is paid by the batch + BEAST_EXPECT(env.balance(carol) == preCarol); + } + + // delegated non atomic inner (AccountSet) + // this also makes sure tfInnerBatchTxn won't block delegated AccountSet + // with granular permission + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + env.fund(XRP(10000), alice, bob, gw); + env.close(); + + env(delegate::set(alice, bob, {"AccountDomainSet"})); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const seq = env.seq(alice); + + auto tx = batch::inner(noop(alice), seq + 1); + std::string const domain = "example.com"; + tx[sfDomain.jsonName] = strHex(domain); + tx[jss::Delegate] = bob.human(); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfAllOrNothing), + tx, + batch::inner(pay(alice, bob, XRP(2)), seq + 2)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "AccountSet", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 3); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(2) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(2)); + } + + // delegated non atomic inner (MPTokenIssuanceSet) + // this also makes sure tfInnerBatchTxn won't block delegated + // MPTokenIssuanceSet with granular permission + { + test::jtx::Env env{*this, envconfig()}; + Account alice{"alice"}; + Account bob{"bob"}; + env.fund(XRP(100000), alice, bob); + env.close(); + + auto const mptID = makeMptID(env.seq(alice), alice); + MPTTester mpt(env, alice, {.fund = false}); + env.close(); + mpt.create({.flags = tfMPTCanLock}); + env.close(); + + // alice gives granular permission to bob of MPTokenIssuanceLock + env(delegate::set( + alice, bob, {"MPTokenIssuanceLock", "MPTokenIssuanceUnlock"})); + env.close(); + + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + + Json::Value jv1; + jv1[sfTransactionType] = jss::MPTokenIssuanceSet; + jv1[sfAccount] = alice.human(); + jv1[sfDelegate] = bob.human(); + jv1[sfSequence] = seq + 1; + jv1[sfMPTokenIssuanceID] = to_string(mptID); + jv1[sfFlags] = tfMPTLock; + + Json::Value jv2; + jv2[sfTransactionType] = jss::MPTokenIssuanceSet; + jv2[sfAccount] = alice.human(); + jv2[sfDelegate] = bob.human(); + jv2[sfSequence] = seq + 2; + jv2[sfMPTokenIssuanceID] = to_string(mptID); + jv2[sfFlags] = tfMPTUnlock; + + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(jv1, seq + 1), + batch::inner(jv2, seq + 2)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "MPTokenIssuanceSet", "tesSUCCESS", txIDs[0], batchID}, + {2, "MPTokenIssuanceSet", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + } + + // delegated non atomic inner (TrustSet) + // this also makes sure tfInnerBatchTxn won't block delegated TrustSet + // with granular permission + { + test::jtx::Env env{*this, envconfig()}; + Account gw{"gw"}; + Account alice{"alice"}; + Account 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", "TrustlineFreeze"})); + env.close(); + + auto const seq = env.seq(gw); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + + auto jv1 = trust(gw, gw["USD"](0), alice, tfSetfAuth); + jv1[sfDelegate] = bob.human(); + auto jv2 = trust(gw, gw["USD"](0), alice, tfSetFreeze); + jv2[sfDelegate] = bob.human(); + + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(gw, seq, batchFee, tfAllOrNothing), + batch::inner(jv1, seq + 1), + batch::inner(jv2, seq + 2)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "TrustSet", "tesSUCCESS", txIDs[0], batchID}, + {2, "TrustSet", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + } + + // inner transaction not authorized by the delegating account. + { + test::jtx::Env env{*this, envconfig()}; + Account gw{"gw"}; + Account alice{"alice"}; + Account 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", "TrustlineFreeze"})); + env.close(); + + auto const seq = env.seq(gw); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + + auto jv1 = trust(gw, gw["USD"](0), alice, tfSetFreeze); + jv1[sfDelegate] = bob.human(); + auto jv2 = trust(gw, gw["USD"](0), alice, tfClearFreeze); + jv2[sfDelegate] = bob.human(); + + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(gw, seq, batchFee, tfIndependent), + batch::inner(jv1, seq + 1), + // tecNO_DELEGATE_PERMISSION: not authorized to clear freeze + batch::inner(jv2, seq + 2)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "TrustSet", "tesSUCCESS", txIDs[0], batchID}, + {2, "TrustSet", "tecNO_DELEGATE_PERMISSION", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + } + } + + void + testValidateRPCResponse(FeatureBitset features) + { + // Verifying that the RPC response from submit includes + // the account_sequence_available, account_sequence_next, + // open_ledger_cost and validated_ledger_index fields. + testcase("Validate RPC response"); + + using namespace jtx; + Env env(*this); + Account const alice("alice"); + Account const bob("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + // tes + { + auto const baseFee = env.current()->fees().base; + auto const aliceSeq = env.seq(alice); + auto jtx = env.jt(pay(alice, bob, XRP(1))); + + Serializer s; + jtx.stx->add(s); + auto const jr = env.rpc("submit", strHex(s.slice()))[jss::result]; + env.close(); + + BEAST_EXPECT(jr.isMember(jss::account_sequence_available)); + BEAST_EXPECT( + jr[jss::account_sequence_available].asUInt() == aliceSeq + 1); + BEAST_EXPECT(jr.isMember(jss::account_sequence_next)); + BEAST_EXPECT( + jr[jss::account_sequence_next].asUInt() == aliceSeq + 1); + BEAST_EXPECT(jr.isMember(jss::open_ledger_cost)); + BEAST_EXPECT(jr[jss::open_ledger_cost] == to_string(baseFee)); + BEAST_EXPECT(jr.isMember(jss::validated_ledger_index)); + } + + // tec failure + { + auto const baseFee = env.current()->fees().base; + auto const aliceSeq = env.seq(alice); + env(fset(bob, asfRequireDest)); + auto jtx = env.jt(pay(alice, bob, XRP(1)), seq(aliceSeq)); + + Serializer s; + jtx.stx->add(s); + auto const jr = env.rpc("submit", strHex(s.slice()))[jss::result]; + env.close(); + + BEAST_EXPECT(jr.isMember(jss::account_sequence_available)); + BEAST_EXPECT( + jr[jss::account_sequence_available].asUInt() == aliceSeq + 1); + BEAST_EXPECT(jr.isMember(jss::account_sequence_next)); + BEAST_EXPECT( + jr[jss::account_sequence_next].asUInt() == aliceSeq + 1); + BEAST_EXPECT(jr.isMember(jss::open_ledger_cost)); + BEAST_EXPECT(jr[jss::open_ledger_cost] == to_string(baseFee)); + BEAST_EXPECT(jr.isMember(jss::validated_ledger_index)); + } + + // tem failure + { + auto const baseFee = env.current()->fees().base; + auto const aliceSeq = env.seq(alice); + auto jtx = env.jt(pay(alice, bob, XRP(1)), seq(aliceSeq + 1)); + + Serializer s; + jtx.stx->add(s); + auto const jr = env.rpc("submit", strHex(s.slice()))[jss::result]; + env.close(); + + BEAST_EXPECT(jr.isMember(jss::account_sequence_available)); + BEAST_EXPECT( + jr[jss::account_sequence_available].asUInt() == aliceSeq); + BEAST_EXPECT(jr.isMember(jss::account_sequence_next)); + BEAST_EXPECT(jr[jss::account_sequence_next].asUInt() == aliceSeq); + BEAST_EXPECT(jr.isMember(jss::open_ledger_cost)); + BEAST_EXPECT(jr[jss::open_ledger_cost] == to_string(baseFee)); + BEAST_EXPECT(jr.isMember(jss::validated_ledger_index)); + } + } + + void + testBatchCalculateBaseFee(FeatureBitset features) + { + using namespace jtx; + Env env(*this); + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + env.fund(XRP(10000), alice, bob, carol); + env.close(); + + auto getBaseFee = [&](JTx const& jtx) -> XRPAmount { + Serializer s; + jtx.stx->add(s); + return Batch::calculateBaseFee(*env.current(), *jtx.stx); + }; + + // bad: Inner Batch transaction found + { + auto const seq = env.seq(alice); + XRPAmount const batchFee = batch::calcBatchFee(env, 0, 2); + auto jtx = env.jt( + batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner( + batch::outer(alice, seq, batchFee, tfAllOrNothing), seq), + batch::inner(pay(alice, bob, XRP(1)), seq + 2)); + XRPAmount const txBaseFee = getBaseFee(jtx); + BEAST_EXPECT(txBaseFee == XRPAmount(INITIAL_XRP)); + } + + // bad: Raw Transactions array exceeds max entries. + { + auto const seq = env.seq(alice); + XRPAmount const batchFee = batch::calcBatchFee(env, 0, 2); + + auto jtx = env.jt( + batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(1)), seq + 2), + batch::inner(pay(alice, bob, XRP(1)), seq + 3), + batch::inner(pay(alice, bob, XRP(1)), seq + 4), + batch::inner(pay(alice, bob, XRP(1)), seq + 5), + batch::inner(pay(alice, bob, XRP(1)), seq + 6), + batch::inner(pay(alice, bob, XRP(1)), seq + 7), + batch::inner(pay(alice, bob, XRP(1)), seq + 8), + batch::inner(pay(alice, bob, XRP(1)), seq + 9)); + + XRPAmount const txBaseFee = getBaseFee(jtx); + BEAST_EXPECT(txBaseFee == XRPAmount(INITIAL_XRP)); + } + + // bad: Signers array exceeds max entries. + { + auto const seq = env.seq(alice); + XRPAmount const batchFee = batch::calcBatchFee(env, 0, 2); + + auto jtx = env.jt( + batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(alice, bob, XRP(5)), seq + 2), + batch::sig( + bob, + carol, + alice, + bob, + carol, + alice, + bob, + carol, + alice, + alice)); + XRPAmount const txBaseFee = getBaseFee(jtx); + BEAST_EXPECT(txBaseFee == XRPAmount(INITIAL_XRP)); + } + + // good: + { + auto const seq = env.seq(alice); + XRPAmount const batchFee = batch::calcBatchFee(env, 0, 2); + auto jtx = env.jt( + batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(bob, alice, XRP(2)), seq + 2)); + XRPAmount const txBaseFee = getBaseFee(jtx); + BEAST_EXPECT(txBaseFee == batchFee); + } + } + + void + testWithFeats(FeatureBitset features) + { + testEnable(features); + testPreflight(features); + testPreclaim(features); + testBadRawTxn(features); + testBadSequence(features); + testBadOuterFee(features); + testCalculateBaseFee(features); + testAllOrNothing(features); + testOnlyOne(features); + testUntilFailure(features); + testIndependent(features); + testInnerSubmitRPC(features); + testAccountActivation(features); + testAccountSet(features); + testAccountDelete(features); + testObjectCreateSequence(features); + testObjectCreateTicket(features); + testObjectCreate3rdParty(features); + testTickets(features); + testSequenceOpenLedger(features); + testTicketsOpenLedger(features); + testObjectsOpenLedger(features); + testPseudoTxn(features); + testOpenLedger(features); + testBatchTxQueue(features); + testBatchNetworkOps(features); + testBatchDelegate(features); + testValidateRPCResponse(features); + testBatchCalculateBaseFee(features); + } + +public: + void + run() override + { + using namespace test::jtx; + auto const sa = testable_amendments(); + testWithFeats(sa); + } +}; + +BEAST_DEFINE_TESTSUITE(Batch, app, ripple); + +} // namespace test +} // namespace ripple diff --git a/src/test/app/Check_test.cpp b/src/test/app/Check_test.cpp index 99b0c8dba3..be38b22313 100644 --- a/src/test/app/Check_test.cpp +++ b/src/test/app/Check_test.cpp @@ -2720,7 +2720,7 @@ public: run() override { using namespace test::jtx; - auto const sa = supported_amendments(); + auto const sa = testable_amendments(); testWithFeats(sa - featureCheckCashMakesTrustLine); testWithFeats(sa - disallowIncoming); testWithFeats(sa); diff --git a/src/test/app/Clawback_test.cpp b/src/test/app/Clawback_test.cpp index d41f6de556..adfe80133a 100644 --- a/src/test/app/Clawback_test.cpp +++ b/src/test/app/Clawback_test.cpp @@ -949,7 +949,7 @@ public: run() override { using namespace test::jtx; - FeatureBitset const all{supported_amendments()}; + FeatureBitset const all{testable_amendments()}; testWithFeats(all - featureMPTokensV1); testWithFeats(all); diff --git a/src/test/app/Credentials_test.cpp b/src/test/app/Credentials_test.cpp index 005ab0cc20..a583050500 100644 --- a/src/test/app/Credentials_test.cpp +++ b/src/test/app/Credentials_test.cpp @@ -1081,7 +1081,7 @@ struct Credentials_test : public beast::unit_test::suite run() override { using namespace test::jtx; - FeatureBitset const all{supported_amendments()}; + FeatureBitset const all{testable_amendments()}; testSuccessful(all); testCredentialsDelete(all); testCreateFailed(all); diff --git a/src/test/app/CrossingLimits_test.cpp b/src/test/app/CrossingLimits_test.cpp index 1e19a178c2..6e76936199 100644 --- a/src/test/app/CrossingLimits_test.cpp +++ b/src/test/app/CrossingLimits_test.cpp @@ -77,10 +77,8 @@ public: auto const gw = Account("gateway"); auto const USD = gw["USD"]; - // The number of allowed offers to cross is different between - // Taker and FlowCross. Taker allows 850 and FlowCross allows 1000. - // Accommodate that difference in the test. - int const maxConsumed = features[featureFlowCross] ? 1000 : 850; + // The payment engine allows 1000 offers to cross. + int const maxConsumed = 1000; env.fund(XRP(100000000), gw, "alice", "bob", "carol"); int const bobsOfferCount = maxConsumed + 150; @@ -119,11 +117,8 @@ public: env.fund(XRP(100000000), gw, "alice", "bob", "carol", "dan", "evita"); - // The number of offers allowed to cross is different between - // Taker and FlowCross. Taker allows 850 and FlowCross allows 1000. - // Accommodate that difference in the test. - bool const isFlowCross{features[featureFlowCross]}; - int const maxConsumed = isFlowCross ? 1000 : 850; + // The payment engine allows 1000 offers to cross. + int const maxConsumed = 1000; int const evitasOfferCount{maxConsumed + 49}; env.trust(USD(1000), "alice"); @@ -133,14 +128,8 @@ public: env.trust(USD(evitasOfferCount + 1), "evita"); env(pay(gw, "evita", USD(evitasOfferCount + 1))); - // Taker and FlowCross have another difference we must accommodate. - // Taker allows a total of 1000 unfunded offers to be consumed - // beyond the 850 offers it can take. FlowCross draws no such - // distinction; its limit is 1000 funded or unfunded. - // - // Give carol an extra 150 (unfunded) offers when we're using Taker - // to accommodate that difference. - int const carolsOfferCount{isFlowCross ? 700 : 850}; + // The payment engine has a limit of 1000 funded or unfunded offers. + int const carolsOfferCount{700}; n_offers(env, 400, "alice", XRP(1), USD(1)); n_offers(env, carolsOfferCount, "carol", XRP(1), USD(1)); n_offers(env, evitasOfferCount, "evita", XRP(1), USD(1)); @@ -268,9 +257,9 @@ public: } void - testAutoBridgedLimitsFlowCross(FeatureBitset features) + testAutoBridgedLimits(FeatureBitset features) { - testcase("Auto Bridged Limits FlowCross"); + testcase("Auto Bridged Limits"); // If any book step in a payment strand consumes 1000 offers, the // liquidity from the offers is used, but that strand will be marked as @@ -452,26 +441,6 @@ public: } } - void - testAutoBridgedLimits(FeatureBitset features) - { - // Taker and FlowCross are too different in the way they handle - // autobridging to make one test suit both approaches. - // - // o Taker alternates between books, completing one full increment - // before returning to make another pass. - // - // o FlowCross extracts as much as possible in one book at one Quality - // before proceeding to the other book. This reduces the number of - // times we change books. - // - // So the tests for the two forms of autobridging are separate. - if (features[featureFlowCross]) - testAutoBridgedLimitsFlowCross(features); - else - testAutoBridgedLimitsTaker(features); - } - void testOfferOverflow(FeatureBitset features) { @@ -522,11 +491,10 @@ public: n_offers(env, 998, alice, XRP(0.96), USD(1)); n_offers(env, 998, alice, XRP(0.95), USD(1)); - bool const withFlowCross = features[featureFlowCross]; bool const withSortStrands = features[featureFlowSortStrands]; auto const expectedTER = [&]() -> TER { - if (withFlowCross && !withSortStrands) + if (!withSortStrands) return TER{tecOVERSIZE}; return tesSUCCESS; }(); @@ -535,8 +503,6 @@ public: env.close(); auto const expectedUSD = [&] { - if (!withFlowCross) - return USD(850); if (!withSortStrands) return USD(0); return USD(1996); @@ -556,10 +522,11 @@ public: testOfferOverflow(features); }; using namespace jtx; - auto const sa = supported_amendments(); + auto const sa = testable_amendments(); testAll(sa); testAll(sa - featureFlowSortStrands); - testAll(sa - featureFlowCross - featureFlowSortStrands); + testAll(sa - featurePermissionedDEX); + testAll(sa - featureFlowSortStrands - featurePermissionedDEX); } }; diff --git a/src/test/app/DID_test.cpp b/src/test/app/DID_test.cpp index 94c0ced162..21fb6b584e 100644 --- a/src/test/app/DID_test.cpp +++ b/src/test/app/DID_test.cpp @@ -382,7 +382,7 @@ struct DID_test : public beast::unit_test::suite run() override { using namespace test::jtx; - FeatureBitset const all{supported_amendments()}; + FeatureBitset const all{testable_amendments()}; FeatureBitset const emptyDID{fixEmptyDID}; testEnabled(all); testAccountReserve(all); diff --git a/src/test/app/Delegate_test.cpp b/src/test/app/Delegate_test.cpp index 5136627148..179532140d 100644 --- a/src/test/app/Delegate_test.cpp +++ b/src/test/app/Delegate_test.cpp @@ -31,7 +31,7 @@ class Delegate_test : public beast::unit_test::suite testcase("test featurePermissionDelegation not enabled"); using namespace jtx; - Env env{*this, supported_amendments() - featurePermissionDelegation}; + Env env{*this, testable_amendments() - featurePermissionDelegation}; Account gw{"gateway"}; Account alice{"alice"}; Account bob{"bob"}; @@ -209,10 +209,10 @@ class Delegate_test : public beast::unit_test::suite } // when authorizing account which does not exist, should return - // terNO_ACCOUNT + // tecNO_TARGET { env(delegate::set(gw, Account("unknown"), {"Payment"}), - ter(terNO_ACCOUNT)); + ter(tecNO_TARGET)); } // non-delegatable transaction @@ -231,6 +231,7 @@ class Delegate_test : public beast::unit_test::suite ter(tecNO_PERMISSION)); env(delegate::set(gw, alice, {"UNLModify"}), ter(tecNO_PERMISSION)); env(delegate::set(gw, alice, {"SetFee"}), ter(tecNO_PERMISSION)); + env(delegate::set(gw, alice, {"Batch"}), ter(tecNO_PERMISSION)); } } @@ -309,8 +310,9 @@ class Delegate_test : public beast::unit_test::suite { // Fee should be checked before permission check, - // otherwise tecNO_PERMISSION returned when permission check fails - // could cause context reset to pay fee because it is tec error + // otherwise tecNO_DELEGATE_PERMISSION returned when permission + // check fails could cause context reset to pay fee because it is + // tec error auto aliceBalance = env.balance(alice); auto bobBalance = env.balance(bob); auto carolBalance = env.balance(carol); @@ -525,12 +527,12 @@ class Delegate_test : public beast::unit_test::suite // bob does not have permission to create check env(check::create(alice, bob, XRP(10)), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); // carol does not have permission to create check env(check::create(alice, bob, XRP(10)), delegate::as(carol), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); } void @@ -565,7 +567,7 @@ class Delegate_test : public beast::unit_test::suite // delegate ledger object is not created yet env(pay(gw, alice, USD(50)), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); env.require(balance(bob, bobBalance - drops(baseFee))); bobBalance = env.balance(bob, XRP); @@ -578,7 +580,7 @@ class Delegate_test : public beast::unit_test::suite // bob sends a payment transaction on behalf of gw env(pay(gw, alice, USD(50)), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); env.close(); env.require(balance(bob, bobBalance - drops(baseFee))); bobBalance = env.balance(bob, XRP); @@ -595,7 +597,7 @@ class Delegate_test : public beast::unit_test::suite // can not send XRP env(pay(gw, alice, XRP(50)), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); env.close(); env.require(balance(bob, bobBalance - drops(baseFee))); bobBalance = env.balance(bob, XRP); @@ -683,7 +685,7 @@ class Delegate_test : public beast::unit_test::suite // permission env(pay(gw, alice, USD(50)), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); env.close(); env.require(balance(bob, bobBalance - drops(baseFee))); bobBalance = env.balance(bob, XRP); @@ -728,7 +730,7 @@ class Delegate_test : public beast::unit_test::suite // has unfreeze permission env(trust(alice, gw["USD"](50)), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); env.close(); // alice creates trustline by herself @@ -742,38 +744,38 @@ class Delegate_test : public beast::unit_test::suite // unsupported flags env(trust(alice, gw["USD"](50), tfSetNoRipple), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); env(trust(alice, gw["USD"](50), tfClearNoRipple), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); env(trust(gw, gw["USD"](0), alice, tfSetDeepFreeze), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); env(trust(gw, gw["USD"](0), alice, tfClearDeepFreeze), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); env.close(); // supported flags with wrong permission env(trust(gw, gw["USD"](0), alice, tfSetfAuth), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); env(trust(gw, gw["USD"](0), alice, tfSetFreeze), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); env.close(); env(delegate::set(gw, bob, {"TrustlineAuthorize"})); env.close(); env(trust(gw, gw["USD"](0), alice, tfClearFreeze), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); env.close(); // although trustline authorize is granted, bob can not change the // limit number env(trust(gw, gw["USD"](50), alice, tfSetfAuth), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); env.close(); // supported flags with correct permission @@ -794,30 +796,30 @@ class Delegate_test : public beast::unit_test::suite // permission env(trust(gw, gw["USD"](0), alice, tfSetFreeze), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); // cannot update LimitAmount with granular permission, both high and // low account env(trust(alice, gw["USD"](100)), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); env(trust(gw, alice["USD"](100)), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); // can not set QualityIn or QualityOut auto tx = trust(alice, gw["USD"](50)); tx["QualityIn"] = "1000"; - env(tx, delegate::as(bob), ter(tecNO_PERMISSION)); + env(tx, delegate::as(bob), ter(tecNO_DELEGATE_PERMISSION)); auto tx2 = trust(alice, gw["USD"](50)); tx2["QualityOut"] = "1000"; - env(tx2, delegate::as(bob), ter(tecNO_PERMISSION)); + env(tx2, delegate::as(bob), ter(tecNO_DELEGATE_PERMISSION)); auto tx3 = trust(gw, alice["USD"](50)); tx3["QualityIn"] = "1000"; - env(tx3, delegate::as(bob), ter(tecNO_PERMISSION)); + env(tx3, delegate::as(bob), ter(tecNO_DELEGATE_PERMISSION)); auto tx4 = trust(gw, alice["USD"](50)); tx4["QualityOut"] = "1000"; - env(tx4, delegate::as(bob), ter(tecNO_PERMISSION)); + env(tx4, delegate::as(bob), ter(tecNO_DELEGATE_PERMISSION)); // granting TrustSet can make it work env(delegate::set(gw, bob, {"TrustSet"})); @@ -827,7 +829,7 @@ class Delegate_test : public beast::unit_test::suite env(tx5, delegate::as(bob)); auto tx6 = trust(alice, gw["USD"](50)); tx6["QualityOut"] = "1000"; - env(tx6, delegate::as(bob), ter(tecNO_PERMISSION)); + env(tx6, delegate::as(bob), ter(tecNO_DELEGATE_PERMISSION)); env(delegate::set(alice, bob, {"TrustSet"})); env.close(); env(tx6, delegate::as(bob)); @@ -846,14 +848,14 @@ class Delegate_test : public beast::unit_test::suite // bob does not have permission env(trust(alice, gw["USD"](50)), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); env(delegate::set( alice, bob, {"TrustlineUnfreeze", "NFTokenCreateOffer"})); env.close(); // bob still does not have permission env(trust(alice, gw["USD"](50)), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); // add TrustSet permission and some unrelated permission env(delegate::set( @@ -892,6 +894,25 @@ class Delegate_test : public beast::unit_test::suite env(trust(alice, gw["USD"](50), tfClearNoRipple), delegate::as(bob)); } + + // tfFullyCanonicalSig won't block delegated transaction + { + Env env(*this); + Account gw{"gw"}; + Account alice{"alice"}; + Account 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 | tfFullyCanonicalSig), + delegate::as(bob)); + } } void @@ -919,16 +940,15 @@ class Delegate_test : public beast::unit_test::suite // on behalf of alice std::string const domain = "example.com"; auto jt = noop(alice); - jt[sfDomain.fieldName] = strHex(domain); - jt[sfDelegate.fieldName] = bob.human(); - jt[sfFlags.fieldName] = tfFullyCanonicalSig; + jt[sfDomain] = strHex(domain); + jt[sfDelegate] = bob.human(); // add granular permission related to AccountSet but is not the // correct permission for domain set env(delegate::set( alice, bob, {"TrustlineUnfreeze", "AccountEmailHashSet"})); env.close(); - env(jt, ter(tecNO_PERMISSION)); + env(jt, ter(tecNO_DELEGATE_PERMISSION)); // alice give granular permission of AccountDomainSet to bob env(delegate::set(alice, bob, {"AccountDomainSet"})); @@ -939,25 +959,24 @@ class Delegate_test : public beast::unit_test::suite BEAST_EXPECT((*env.le(alice))[sfDomain] == makeSlice(domain)); // bob can reset domain - jt[sfDomain.fieldName] = ""; + jt[sfDomain] = ""; env(jt); BEAST_EXPECT(!env.le(alice)->isFieldPresent(sfDomain)); - // if flag is not equal to tfFullyCanonicalSig, which means bob - // is trying to set the flag at the same time, it will fail + // bob tries to set unauthorized flag, it will fail std::string const failDomain = "fail_domain_update"; - jt[sfFlags.fieldName] = tfRequireAuth; - jt[sfDomain.fieldName] = strHex(failDomain); - env(jt, ter(tecNO_PERMISSION)); + jt[sfFlags] = tfRequireAuth; + jt[sfDomain] = strHex(failDomain); + env(jt, ter(tecNO_DELEGATE_PERMISSION)); // reset flag number - jt[sfFlags.fieldName] = tfFullyCanonicalSig; + jt[sfFlags] = 0; // bob tries to update domain and set email hash, // but he does not have permission to set email hash - jt[sfDomain.fieldName] = strHex(domain); + jt[sfDomain] = strHex(domain); std::string const mh("5F31A79367DC3137FADA860C05742EE6"); - jt[sfEmailHash.fieldName] = mh; - env(jt, ter(tecNO_PERMISSION)); + jt[sfEmailHash] = mh; + env(jt, ter(tecNO_DELEGATE_PERMISSION)); // alice give granular permission of AccountEmailHashSet to bob env(delegate::set( @@ -969,8 +988,8 @@ class Delegate_test : public beast::unit_test::suite // bob does not have permission to set message key for alice auto const rkp = randomKeyPair(KeyType::ed25519); - jt[sfMessageKey.fieldName] = strHex(rkp.first.slice()); - env(jt, ter(tecNO_PERMISSION)); + jt[sfMessageKey] = strHex(rkp.first.slice()); + env(jt, ter(tecNO_DELEGATE_PERMISSION)); // alice give granular permission of AccountMessageKeySet to bob env(delegate::set( @@ -986,12 +1005,14 @@ class Delegate_test : public beast::unit_test::suite BEAST_EXPECT( strHex((*env.le(alice))[sfMessageKey]) == strHex(rkp.first.slice())); - jt[sfMessageKey.fieldName] = ""; + jt[sfMessageKey] = ""; env(jt); BEAST_EXPECT(!env.le(alice)->isFieldPresent(sfMessageKey)); // bob does not have permission to set transfer rate for alice - env(rate(alice, 2.0), delegate::as(bob), ter(tecNO_PERMISSION)); + env(rate(alice, 2.0), + delegate::as(bob), + ter(tecNO_DELEGATE_PERMISSION)); // alice give granular permission of AccountTransferRateSet to bob env(delegate::set( @@ -1003,14 +1024,13 @@ class Delegate_test : public beast::unit_test::suite "AccountTransferRateSet"})); env.close(); auto jtRate = rate(alice, 2.0); - jtRate[sfDelegate.fieldName] = bob.human(); - jtRate[sfFlags.fieldName] = tfFullyCanonicalSig; + jtRate[sfDelegate] = bob.human(); env(jtRate, delegate::as(bob)); BEAST_EXPECT((*env.le(alice))[sfTransferRate] == 2000000000); // bob does not have permission to set ticksize for alice - jt[sfTickSize.fieldName] = 8; - env(jt, ter(tecNO_PERMISSION)); + jt[sfTickSize] = 8; + env(jt, ter(tecNO_DELEGATE_PERMISSION)); // alice give granular permission of AccountTickSizeSet to bob env(delegate::set( @@ -1028,7 +1048,7 @@ class Delegate_test : public beast::unit_test::suite // can not set asfRequireAuth flag for alice env(fset(alice, asfRequireAuth), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); // reset Delegate will delete the Delegate // object @@ -1037,15 +1057,15 @@ class Delegate_test : public beast::unit_test::suite // alice env(fset(alice, asfRequireAuth), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); // alice can set for herself env(fset(alice, asfRequireAuth)); env.require(flags(alice, asfRequireAuth)); env.close(); // can not update tick size because bob no longer has permission - jt[sfTickSize.fieldName] = 7; - env(jt, ter(tecNO_PERMISSION)); + jt[sfTickSize] = 7; + env(jt, ter(tecNO_DELEGATE_PERMISSION)); env(delegate::set( alice, @@ -1059,12 +1079,11 @@ class Delegate_test : public beast::unit_test::suite std::string const locator = "9633EC8AF54F16B5286DB1D7B519EF49EEFC050C0C8AC4384F1D88ACD1BFDF" "05"; - auto jt2 = noop(alice); - jt2[sfDomain.fieldName] = strHex(domain); - jt2[sfDelegate.fieldName] = bob.human(); - jt2[sfWalletLocator.fieldName] = locator; - jt2[sfFlags.fieldName] = tfFullyCanonicalSig; - env(jt2, ter(tecNO_PERMISSION)); + auto jv2 = noop(alice); + jv2[sfDomain] = strHex(domain); + jv2[sfDelegate] = bob.human(); + jv2[sfWalletLocator] = locator; + env(jv2, ter(tecNO_DELEGATE_PERMISSION)); } // can not set AccountSet flags on behalf of other account @@ -1079,7 +1098,7 @@ class Delegate_test : public beast::unit_test::suite // bob can not set flag on behalf of alice env(fset(alice, flag), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); // alice set by herself env(fset(alice, flag)); env.close(); @@ -1087,7 +1106,7 @@ class Delegate_test : public beast::unit_test::suite // bob can not clear on behalf of alice env(fclear(alice, flag), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); }; // testSetClearFlag(asfNoFreeze); @@ -1116,19 +1135,19 @@ class Delegate_test : public beast::unit_test::suite // bob can not set asfAccountTxnID on behalf of alice env(fset(alice, asfAccountTxnID), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); env(fset(alice, asfAccountTxnID)); env.close(); BEAST_EXPECT(env.le(alice)->isFieldPresent(sfAccountTxnID)); env(fclear(alice, asfAccountTxnID), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); // bob can not set asfAuthorizedNFTokenMinter on behalf of alice Json::Value jt = fset(alice, asfAuthorizedNFTokenMinter); - jt[sfDelegate.fieldName] = bob.human(); - jt[sfNFTokenMinter.fieldName] = bob.human(); - env(jt, ter(tecNO_PERMISSION)); + jt[sfDelegate] = bob.human(); + jt[sfNFTokenMinter] = bob.human(); + env(jt, ter(tecNO_DELEGATE_PERMISSION)); // bob gives alice some permissions env(delegate::set( @@ -1144,14 +1163,14 @@ class Delegate_test : public beast::unit_test::suite // behalf of bob. env(fset(alice, asfNoFreeze), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); env(fset(bob, asfNoFreeze)); env.close(); env.require(flags(bob, asfNoFreeze)); // alice can not clear on behalf of bob env(fclear(alice, asfNoFreeze), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); // bob can not set asfDisableMaster on behalf of alice Account const bobKey{"bobKey", KeyType::secp256k1}; @@ -1160,7 +1179,29 @@ class Delegate_test : public beast::unit_test::suite env(fset(alice, asfDisableMaster), delegate::as(bob), sig(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); + } + + // tfFullyCanonicalSig won't block delegated transaction + { + Env env(*this); + Account alice{"alice"}; + Account bob{"bob"}; + env.fund(XRP(10000), alice, bob); + env.close(); + + env(delegate::set( + alice, bob, {"AccountDomainSet", "AccountEmailHashSet"})); + env.close(); + + std::string const domain = "example.com"; + auto jt = noop(alice); + jt[sfDomain] = strHex(domain); + jt[sfDelegate] = bob.human(); + jt[sfFlags] = tfFullyCanonicalSig; + + env(jt); + BEAST_EXPECT((*env.le(alice))[sfDomain] == makeSlice(domain)); } } @@ -1188,7 +1229,7 @@ class Delegate_test : public beast::unit_test::suite {.account = alice, .flags = tfMPTLock, .delegate = bob, - .err = tecNO_PERMISSION}); + .err = tecNO_DELEGATE_PERMISSION}); // alice gives granular permission to bob of MPTokenIssuanceUnlock env(delegate::set(alice, bob, {"MPTokenIssuanceUnlock"})); @@ -1198,7 +1239,7 @@ class Delegate_test : public beast::unit_test::suite {.account = alice, .flags = tfMPTLock, .delegate = bob, - .err = tecNO_PERMISSION}); + .err = tecNO_DELEGATE_PERMISSION}); // bob now has lock permission, but does not have unlock permission env(delegate::set(alice, bob, {"MPTokenIssuanceLock"})); env.close(); @@ -1207,7 +1248,7 @@ class Delegate_test : public beast::unit_test::suite {.account = alice, .flags = tfMPTUnlock, .delegate = bob, - .err = tecNO_PERMISSION}); + .err = tecNO_DELEGATE_PERMISSION}); // now bob can lock and unlock env(delegate::set( @@ -1240,7 +1281,7 @@ class Delegate_test : public beast::unit_test::suite {.account = alice, .flags = tfMPTUnlock, .delegate = bob, - .err = tecNO_PERMISSION}); + .err = tecNO_DELEGATE_PERMISSION}); // alice gives bob some unrelated permission with // MPTokenIssuanceLock @@ -1254,7 +1295,7 @@ class Delegate_test : public beast::unit_test::suite {.account = alice, .flags = tfMPTUnlock, .delegate = bob, - .err = tecNO_PERMISSION}); + .err = tecNO_DELEGATE_PERMISSION}); // alice add MPTokenIssuanceSet to permissions env(delegate::set( @@ -1270,6 +1311,28 @@ class Delegate_test : public beast::unit_test::suite mpt.set({.account = alice, .flags = tfMPTUnlock, .delegate = bob}); mpt.set({.account = alice, .flags = tfMPTLock, .delegate = bob}); } + + // tfFullyCanonicalSig won't block delegated transaction + { + Env env(*this); + Account alice{"alice"}; + Account bob{"bob"}; + env.fund(XRP(100000), alice, bob); + env.close(); + + MPTTester mpt(env, alice, {.fund = false}); + env.close(); + mpt.create({.flags = tfMPTCanLock}); + env.close(); + + // alice gives granular permission to bob of MPTokenIssuanceLock + env(delegate::set(alice, bob, {"MPTokenIssuanceLock"})); + env.close(); + mpt.set( + {.account = alice, + .flags = tfMPTLock | tfFullyCanonicalSig, + .delegate = bob}); + } } void diff --git a/src/test/app/DeliverMin_test.cpp b/src/test/app/DeliverMin_test.cpp index b079b93680..a9373fb002 100644 --- a/src/test/app/DeliverMin_test.cpp +++ b/src/test/app/DeliverMin_test.cpp @@ -142,8 +142,8 @@ public: run() override { using namespace jtx; - auto const sa = supported_amendments(); - test_convert_all_of_an_asset(sa - featureFlowCross); + auto const sa = testable_amendments(); + test_convert_all_of_an_asset(sa - featurePermissionedDEX); test_convert_all_of_an_asset(sa); } }; diff --git a/src/test/app/DepositAuth_test.cpp b/src/test/app/DepositAuth_test.cpp index c8dc3c00eb..ffe8c4448b 100644 --- a/src/test/app/DepositAuth_test.cpp +++ b/src/test/app/DepositAuth_test.cpp @@ -53,7 +53,7 @@ struct DepositAuth_test : public beast::unit_test::suite { // featureDepositAuth is disabled. - Env env(*this, supported_amendments() - featureDepositAuth); + Env env(*this, testable_amendments() - featureDepositAuth); env.fund(XRP(10000), alice); // Note that, to support old behavior, invalid flags are ignored. @@ -352,27 +352,27 @@ struct DepositAuth_test : public beast::unit_test::suite auto const noRippleNext = i & 0x2; auto const withDepositAuth = i & 0x4; testIssuer( - supported_amendments() | featureDepositAuth, + testable_amendments() | featureDepositAuth, noRipplePrev, noRippleNext, withDepositAuth); if (!withDepositAuth) testIssuer( - supported_amendments() - featureDepositAuth, + testable_amendments() - featureDepositAuth, noRipplePrev, noRippleNext, withDepositAuth); testNonIssuer( - supported_amendments() | featureDepositAuth, + testable_amendments() | featureDepositAuth, noRipplePrev, noRippleNext, withDepositAuth); if (!withDepositAuth) testNonIssuer( - supported_amendments() - featureDepositAuth, + testable_amendments() - featureDepositAuth, noRipplePrev, noRippleNext, withDepositAuth); @@ -420,7 +420,7 @@ struct DepositPreauth_test : public beast::unit_test::suite Account const becky{"becky"}; { // featureDepositPreauth is disabled. - Env env(*this, supported_amendments() - featureDepositPreauth); + Env env(*this, testable_amendments() - featureDepositPreauth); env.fund(XRP(10000), alice, becky); env.close(); @@ -714,12 +714,12 @@ struct DepositPreauth_test : public beast::unit_test::suite if (!supportsPreauth) { auto const seq1 = env.seq(alice); - env(escrow(alice, becky, XRP(100)), - finish_time(env.now() + 1s)); + env(escrow::create(alice, becky, XRP(100)), + escrow::finish_time(env.now() + 1s)); env.close(); // Failed as rule is disabled - env(finish(gw, alice, seq1), + env(escrow::finish(gw, alice, seq1), fee(1500), ter(tecNO_PERMISSION)); env.close(); @@ -830,7 +830,7 @@ struct DepositPreauth_test : public beast::unit_test::suite { testcase("Payment failure with disabled credentials rule."); - Env env(*this, supported_amendments() - featureCredentials); + Env env(*this, testable_amendments() - featureCredentials); env.fund(XRP(5000), issuer, bob, alice); env.close(); @@ -1387,12 +1387,13 @@ struct DepositPreauth_test : public beast::unit_test::suite env.close(); auto const seq = env.seq(alice); - env(escrow(alice, bob, XRP(1000)), finish_time(env.now() + 1s)); + env(escrow::create(alice, bob, XRP(1000)), + escrow::finish_time(env.now() + 1s)); env.close(); // zelda can't finish escrow with invalid credentials { - env(finish(zelda, alice, seq), + env(escrow::finish(zelda, alice, seq), credentials::ids({}), ter(temMALFORMED)); env.close(); @@ -1404,14 +1405,14 @@ struct DepositPreauth_test : public beast::unit_test::suite "0E0B04ED60588A758B67E21FBBE95AC5A63598BA951761DC0EC9C08D7E" "01E034"; - env(finish(zelda, alice, seq), + env(escrow::finish(zelda, alice, seq), credentials::ids({invalidIdx}), ter(tecBAD_CREDENTIALS)); env.close(); } { // Ledger closed, time increased, zelda can't finish escrow - env(finish(zelda, alice, seq), + env(escrow::finish(zelda, alice, seq), credentials::ids({credIdx}), fee(1500), ter(tecEXPIRED)); @@ -1562,7 +1563,7 @@ struct DepositPreauth_test : public beast::unit_test::suite { testEnable(); testInvalid(); - auto const supported{jtx::supported_amendments()}; + auto const supported{jtx::testable_amendments()}; testPayment(supported - featureDepositPreauth - featureCredentials); testPayment(supported - featureDepositPreauth); testPayment(supported - featureCredentials); diff --git a/src/test/app/Discrepancy_test.cpp b/src/test/app/Discrepancy_test.cpp index 8e306282a7..da41969885 100644 --- a/src/test/app/Discrepancy_test.cpp +++ b/src/test/app/Discrepancy_test.cpp @@ -146,8 +146,8 @@ public: run() override { using namespace test::jtx; - auto const sa = supported_amendments(); - testXRPDiscrepancy(sa - featureFlowCross); + auto const sa = testable_amendments(); + testXRPDiscrepancy(sa - featurePermissionedDEX); testXRPDiscrepancy(sa); } }; diff --git a/src/test/app/EscrowToken_test.cpp b/src/test/app/EscrowToken_test.cpp new file mode 100644 index 0000000000..e81064c825 --- /dev/null +++ b/src/test/app/EscrowToken_test.cpp @@ -0,0 +1,3887 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +namespace ripple { +namespace test { + +struct EscrowToken_test : public beast::unit_test::suite +{ + static uint64_t + mptEscrowed( + jtx::Env const& env, + jtx::Account const& account, + jtx::MPT const& mpt) + { + auto const sle = env.le(keylet::mptoken(mpt.mpt(), account)); + if (sle && sle->isFieldPresent(sfLockedAmount)) + return (*sle)[sfLockedAmount]; + return 0; + } + + static uint64_t + issuerMPTEscrowed(jtx::Env const& env, jtx::MPT const& mpt) + { + auto const sle = env.le(keylet::mptIssuance(mpt.mpt())); + if (sle && sle->isFieldPresent(sfLockedAmount)) + return (*sle)[sfLockedAmount]; + return 0; + } + + jtx::PrettyAmount + issuerBalance( + jtx::Env& env, + jtx::Account const& account, + Issue const& issue) + { + Json::Value params; + params[jss::account] = account.human(); + auto jrr = env.rpc("json", "gateway_balances", to_string(params)); + auto const result = jrr[jss::result]; + auto const obligations = + result[jss::obligations][to_string(issue.currency)]; + if (obligations.isNull()) + return {STAmount(issue, 0), account.name()}; + STAmount const amount = amountFromString(issue, obligations.asString()); + return {amount, account.name()}; + } + + jtx::PrettyAmount + issuerEscrowed( + jtx::Env& env, + jtx::Account const& account, + Issue const& issue) + { + Json::Value params; + params[jss::account] = account.human(); + auto jrr = env.rpc("json", "gateway_balances", to_string(params)); + auto const result = jrr[jss::result]; + auto const locked = result[jss::locked][to_string(issue.currency)]; + if (locked.isNull()) + return {STAmount(issue, 0), account.name()}; + STAmount const amount = amountFromString(issue, locked.asString()); + return {amount, account.name()}; + } + + void + testIOUEnablement(FeatureBitset features) + { + testcase("IOU Enablement"); + + using namespace jtx; + using namespace std::chrono; + + for (bool const withTokenEscrow : {false, true}) + { + auto const amend = + withTokenEscrow ? features : features - featureTokenEscrow; + Env env{*this, amend}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env.close(); + + auto const createResult = + withTokenEscrow ? ter(tesSUCCESS) : ter(temBAD_AMOUNT); + auto const finishResult = + withTokenEscrow ? ter(tesSUCCESS) : ter(tecNO_TARGET); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, USD(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + createResult); + env.close(); + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + finishResult); + env.close(); + + auto const seq2 = env.seq(alice); + env(escrow::create(alice, bob, USD(1'000)), + escrow::condition(escrow::cb2), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s), + fee(baseFee * 150), + createResult); + env.close(); + env(escrow::cancel(bob, alice, seq2), finishResult); + env.close(); + } + + for (bool const withTokenEscrow : {false, true}) + { + auto const amend = + withTokenEscrow ? features : features - featureTokenEscrow; + Env env{*this, amend}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tecNO_TARGET)); + env.close(); + + env(escrow::cancel(bob, alice, seq1), ter(tecNO_TARGET)); + env.close(); + } + } + + void + testIOUAllowLockingFlag(FeatureBitset features) + { + testcase("IOU Allow Locking Flag"); + + using namespace jtx; + using namespace std::chrono; + + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env.close(); + + // Create Escrow #1 & #2 + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, USD(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + auto const seq2 = env.seq(alice); + env(escrow::create(alice, bob, USD(1'000)), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 3s), + fee(baseFee), + ter(tesSUCCESS)); + env.close(); + + // Clear the asfAllowTrustLineLocking flag + env(fclear(gw, asfAllowTrustLineLocking)); + env.close(); + env.require(nflags(gw, asfAllowTrustLineLocking)); + + // Cannot Create Escrow without asfAllowTrustLineLocking + env(escrow::create(alice, bob, USD(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_PERMISSION)); + env.close(); + + // Can finish the escrow created before the flag was cleared + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + // Can cancel the escrow created before the flag was cleared + env(escrow::cancel(bob, alice, seq2), ter(tesSUCCESS)); + env.close(); + } + + void + testIOUCreatePreflight(FeatureBitset features) + { + testcase("IOU Create Preflight"); + using namespace test::jtx; + using namespace std::literals; + + // temBAD_FEE: Exercises invalid preflight1. + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + + env(escrow::create(alice, bob, USD(1)), + escrow::finish_time(env.now() + 1s), + fee(XRP(-1)), + ter(temBAD_FEE)); + env.close(); + } + + // temBAD_AMOUNT: amount <= 0 + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + + env(escrow::create(alice, bob, USD(-1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(temBAD_AMOUNT)); + env.close(); + } + + // temBAD_CURRENCY: badCurrency() == amount.getCurrency() + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const BAD = IOU(gw, badCurrency()); + env.fund(XRP(5000), alice, bob, gw); + + env(escrow::create(alice, bob, BAD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(temBAD_CURRENCY)); + env.close(); + } + } + + void + testIOUCreatePreclaim(FeatureBitset features) + { + testcase("IOU Create Preclaim"); + using namespace test::jtx; + using namespace std::literals; + + // tecNO_PERMISSION: issuer is the same as the account + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + + env(escrow::create(gw, alice, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_PERMISSION)); + env.close(); + } + + // tecNO_ISSUER: Issuer does not exist + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob); + env.close(); + env.memoize(gw); + + env(escrow::create(alice, bob, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_ISSUER)); + env.close(); + } + + // tecNO_PERMISSION: asfAllowTrustLineLocking is not set + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env.close(); + env.trust(USD(10'000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env.close(); + + env(escrow::create(gw, alice, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_PERMISSION)); + env.close(); + } + + // tecNO_LINE: account does not have a trustline to the issuer + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env(escrow::create(alice, bob, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_LINE)); + env.close(); + } + + // tecNO_PERMISSION: Not testable + // tecNO_PERMISSION: Not testable + // tecNO_AUTH: requireAuth + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(fset(gw, asfRequireAuth)); + env.close(); + env.trust(USD(10'000), alice, bob); + env.close(); + + env(escrow::create(alice, bob, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_AUTH)); + env.close(); + } + + // tecNO_AUTH: requireAuth + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + auto const aliceUSD = alice["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(fset(gw, asfRequireAuth)); + env.close(); + env(trust(gw, aliceUSD(10'000)), txflags(tfSetfAuth)); + env.trust(USD(10'000), alice, bob); + env.close(); + + env(escrow::create(alice, bob, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_AUTH)); + env.close(); + } + + // tecFROZEN: account is frozen + { + // Env Setup + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env(trust(alice, USD(100'000))); + env(trust(bob, USD(100'000))); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + // set freeze on alice trustline + env(trust(gw, USD(10'000), alice, tfSetFreeze)); + env.close(); + + env(escrow::create(alice, bob, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecFROZEN)); + env.close(); + } + + // tecFROZEN: dest is frozen + { + // Env Setup + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env(trust(alice, USD(100'000))); + env(trust(bob, USD(100'000))); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + // set freeze on bob trustline + env(trust(gw, USD(10'000), bob, tfSetFreeze)); + env.close(); + + env(escrow::create(alice, bob, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecFROZEN)); + env.close(); + } + + // tecINSUFFICIENT_FUNDS + { + // Env Setup + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env(trust(alice, USD(100'000))); + env(trust(bob, USD(100'000))); + env.close(); + + env(escrow::create(alice, bob, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecINSUFFICIENT_FUNDS)); + env.close(); + } + + // tecINSUFFICIENT_FUNDS + { + // Env Setup + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env(trust(alice, USD(100'000))); + env(trust(bob, USD(100'000))); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + env(escrow::create(alice, bob, USD(10'001)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecINSUFFICIENT_FUNDS)); + env.close(); + } + + // tecPRECISION_LOSS + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(100000000000000000), alice); + env.trust(USD(100000000000000000), bob); + env.close(); + env(pay(gw, alice, USD(10000000000000000))); + env(pay(gw, bob, USD(1))); + env.close(); + + // alice cannot create escrow for 1/10 iou - precision loss + env(escrow::create(alice, bob, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecPRECISION_LOSS)); + env.close(); + } + } + + void + testIOUFinishPreclaim(FeatureBitset features) + { + testcase("IOU Finish Preclaim"); + using namespace test::jtx; + using namespace std::literals; + + // tecNO_AUTH: requireAuth set: dest not authorized + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + auto const aliceUSD = alice["USD"]; + auto const bobUSD = bob["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(fset(gw, asfRequireAuth)); + env.close(); + env(trust(gw, aliceUSD(10'000)), txflags(tfSetfAuth)); + env(trust(gw, bobUSD(10'000)), txflags(tfSetfAuth)); + env.trust(USD(10'000), alice, bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + env(pay(bob, gw, USD(10'000))); + env(trust(gw, bobUSD(0)), txflags(tfSetfAuth)); + env(trust(bob, USD(0))); + env.close(); + + env.trust(USD(10'000), bob); + env.close(); + + // bob cannot finish because he is not authorized + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tecNO_AUTH)); + env.close(); + } + + // tecFROZEN: issuer has deep frozen the dest + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice, bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + // set freeze on bob trustline + env(trust(gw, USD(10'000), bob, tfSetFreeze | tfSetDeepFreeze)); + + // bob cannot finish because of deep freeze + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tecFROZEN)); + env.close(); + } + } + + void + testIOUFinishDoApply(FeatureBitset features) + { + testcase("IOU Finish Do Apply"); + using namespace test::jtx; + using namespace std::literals; + + // tecNO_LINE_INSUF_RESERVE: insufficient reserve to create line + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const acctReserve = env.current()->fees().accountReserve(0); + auto const incReserve = env.current()->fees().increment; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, gw); + env.fund(acctReserve + (incReserve - 1), bob); + env.close(); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice); + env.close(); + env(pay(gw, alice, USD(10'000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + // bob cannot finish because insufficient reserve to create line + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tecNO_LINE_INSUF_RESERVE)); + env.close(); + } + + // tecNO_LINE: alice submits; finish IOU not created + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env.close(); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice); + env.close(); + env(pay(gw, alice, USD(10'000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + // alice cannot finish because bob does not have a trustline + env(escrow::finish(alice, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tecNO_LINE)); + env.close(); + } + + // tecLIMIT_EXCEEDED: alice submits; IOU Limit < balance + amount + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env.close(); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(1000), alice, bob); + env.close(); + env(pay(gw, alice, USD(1000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, USD(5)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + env.trust(USD(1), bob); + env.close(); + + // alice cannot finish because bobs limit is too low + env(escrow::finish(alice, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tecLIMIT_EXCEEDED)); + env.close(); + } + + // tesSUCCESS: bob submits; IOU Limit < balance + amount + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env.close(); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(1000), alice, bob); + env.close(); + env(pay(gw, alice, USD(1000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, USD(5)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + env.trust(USD(1), bob); + env.close(); + + // bob can finish even if bobs limit is too low + auto const bobPreLimit = env.limit(bob, USD); + + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + // bobs limit is not changed + BEAST_EXPECT(env.limit(bob, USD) == bobPreLimit); + } + } + + void + testIOUCancelPreclaim(FeatureBitset features) + { + testcase("IOU Cancel Preclaim"); + using namespace test::jtx; + using namespace std::literals; + + // tecNO_AUTH: requireAuth set: account not authorized + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + auto const aliceUSD = alice["USD"]; + auto const bobUSD = bob["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(fset(gw, asfRequireAuth)); + env.close(); + env(trust(gw, aliceUSD(10'000)), txflags(tfSetfAuth)); + env(trust(gw, bobUSD(10'000)), txflags(tfSetfAuth)); + env.trust(USD(10'000), alice, bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, USD(1)), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s), + fee(baseFee), + ter(tesSUCCESS)); + env.close(); + + env(pay(alice, gw, USD(9'999))); + env(trust(gw, aliceUSD(0)), txflags(tfSetfAuth)); + env(trust(alice, USD(0))); + env.close(); + + env.trust(USD(10'000), alice); + env.close(); + + // alice cannot cancel because she is not authorized + env(escrow::cancel(bob, alice, seq1), + fee(baseFee), + ter(tecNO_AUTH)); + env.close(); + } + } + + void + testIOUBalances(FeatureBitset features) + { + testcase("IOU Balances"); + + using namespace jtx; + using namespace std::chrono; + + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5'000))); + env(pay(gw, bob, USD(5'000))); + env.close(); + + auto const outstandingUSD = USD(10'000); + + // Create & Finish Escrow + auto const seq1 = env.seq(alice); + { + auto const preAliceUSD = env.balance(alice, USD); + auto const preBobUSD = env.balance(bob, USD); + env(escrow::create(alice, bob, USD(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, USD) == preAliceUSD - USD(1'000)); + BEAST_EXPECT(env.balance(bob, USD) == preBobUSD); + BEAST_EXPECT( + issuerBalance(env, gw, USD) == outstandingUSD - USD(1'000)); + BEAST_EXPECT(issuerEscrowed(env, gw, USD) == USD(1'000)); + } + { + auto const preAliceUSD = env.balance(alice, USD); + auto const preBobUSD = env.balance(bob, USD); + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, USD) == preAliceUSD); + BEAST_EXPECT(env.balance(bob, USD) == preBobUSD + USD(1'000)); + BEAST_EXPECT(issuerBalance(env, gw, USD) == outstandingUSD); + BEAST_EXPECT(issuerEscrowed(env, gw, USD) == USD(0)); + } + + // Create & Cancel Escrow + auto const seq2 = env.seq(alice); + { + auto const preAliceUSD = env.balance(alice, USD); + auto const preBobUSD = env.balance(bob, USD); + env(escrow::create(alice, bob, USD(1'000)), + escrow::condition(escrow::cb2), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, USD) == preAliceUSD - USD(1'000)); + BEAST_EXPECT(env.balance(bob, USD) == preBobUSD); + BEAST_EXPECT( + issuerBalance(env, gw, USD) == outstandingUSD - USD(1'000)); + BEAST_EXPECT(issuerEscrowed(env, gw, USD) == USD(1'000)); + } + { + auto const preAliceUSD = env.balance(alice, USD); + auto const preBobUSD = env.balance(bob, USD); + env(escrow::cancel(bob, alice, seq2), ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, USD) == preAliceUSD + USD(1'000)); + BEAST_EXPECT(env.balance(bob, USD) == preBobUSD); + BEAST_EXPECT(issuerBalance(env, gw, USD) == outstandingUSD); + BEAST_EXPECT(issuerEscrowed(env, gw, USD) == USD(0)); + } + } + + void + testIOUMetaAndOwnership(FeatureBitset features) + { + using namespace jtx; + using namespace std::chrono; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + { + testcase("IOU Metadata to self"); + + Env env{*this, features}; + env.fund(XRP(5000), alice, bob, carol, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice, bob, carol); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env(pay(gw, carol, USD(5000))); + env.close(); + auto const aseq = env.seq(alice); + auto const bseq = env.seq(bob); + + env(escrow::create(alice, alice, USD(1'000)), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 500s)); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + env.close(5s); + auto const aa = env.le(keylet::escrow(alice.id(), aseq)); + BEAST_EXPECT(aa); + { + ripple::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 2); + BEAST_EXPECT( + std::find(aod.begin(), aod.end(), aa) != aod.end()); + } + + { + ripple::Dir iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 4); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), aa) != iod.end()); + } + + env(escrow::create(bob, bob, USD(1'000)), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s)); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + env.close(5s); + auto const bb = env.le(keylet::escrow(bob.id(), bseq)); + BEAST_EXPECT(bb); + + { + ripple::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 2); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), bb) != bod.end()); + } + + { + ripple::Dir iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 5); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), bb) != iod.end()); + } + + env.close(5s); + env(escrow::finish(alice, alice, aseq)); + { + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + + ripple::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); + BEAST_EXPECT( + std::find(aod.begin(), aod.end(), aa) == aod.end()); + + ripple::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 2); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), bb) != bod.end()); + + ripple::Dir iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 4); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), bb) != iod.end()); + } + + env.close(5s); + env(escrow::cancel(bob, bob, bseq)); + { + BEAST_EXPECT(!env.le(keylet::escrow(bob.id(), bseq))); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + + ripple::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 1); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), bb) == bod.end()); + + ripple::Dir iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 3); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), bb) == iod.end()); + } + } + { + testcase("IOU Metadata to other"); + + Env env{*this, features}; + env.fund(XRP(5000), alice, bob, carol, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice, bob, carol); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env(pay(gw, carol, USD(5000))); + env.close(); + auto const aseq = env.seq(alice); + auto const bseq = env.seq(bob); + + env(escrow::create(alice, bob, USD(1'000)), + escrow::finish_time(env.now() + 1s)); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + env.close(5s); + env(escrow::create(bob, carol, USD(1'000)), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s)); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + env.close(5s); + + auto const ab = env.le(keylet::escrow(alice.id(), aseq)); + BEAST_EXPECT(ab); + + auto const bc = env.le(keylet::escrow(bob.id(), bseq)); + BEAST_EXPECT(bc); + + { + ripple::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 2); + BEAST_EXPECT( + std::find(aod.begin(), aod.end(), ab) != aod.end()); + + ripple::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 3); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), ab) != bod.end()); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), bc) != bod.end()); + + ripple::Dir cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 2); + BEAST_EXPECT( + std::find(cod.begin(), cod.end(), bc) != cod.end()); + + ripple::Dir iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 5); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), ab) != iod.end()); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), bc) != iod.end()); + } + + env.close(5s); + env(escrow::finish(alice, alice, aseq)); + { + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); + BEAST_EXPECT(env.le(keylet::escrow(bob.id(), bseq))); + + ripple::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); + BEAST_EXPECT( + std::find(aod.begin(), aod.end(), ab) == aod.end()); + + ripple::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 2); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), ab) == bod.end()); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), bc) != bod.end()); + + ripple::Dir cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 2); + + ripple::Dir iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 4); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), ab) == iod.end()); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), bc) != iod.end()); + } + + env.close(5s); + env(escrow::cancel(bob, bob, bseq)); + { + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); + BEAST_EXPECT(!env.le(keylet::escrow(bob.id(), bseq))); + + ripple::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); + BEAST_EXPECT( + std::find(aod.begin(), aod.end(), ab) == aod.end()); + + ripple::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 1); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), ab) == bod.end()); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), bc) == bod.end()); + + ripple::Dir cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 1); + + ripple::Dir iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 3); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), ab) == iod.end()); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), bc) == iod.end()); + } + } + + { + testcase("IOU Metadata to issuer"); + + Env env{*this, features}; + env.fund(XRP(5000), alice, carol, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice, carol); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, carol, USD(5000))); + env.close(); + auto const aseq = env.seq(alice); + + env(escrow::create(alice, gw, USD(1'000)), + escrow::finish_time(env.now() + 1s)); + + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + env.close(5s); + env(escrow::create(gw, carol, USD(1'000)), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s), + ter(tecNO_PERMISSION)); + env.close(5s); + + auto const ag = env.le(keylet::escrow(alice.id(), aseq)); + BEAST_EXPECT(ag); + + { + ripple::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 2); + BEAST_EXPECT( + std::find(aod.begin(), aod.end(), ag) != aod.end()); + + ripple::Dir cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 1); + + ripple::Dir iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 3); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), ag) != iod.end()); + } + + env.close(5s); + env(escrow::finish(alice, alice, aseq)); + { + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); + + ripple::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); + BEAST_EXPECT( + std::find(aod.begin(), aod.end(), ag) == aod.end()); + + ripple::Dir cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 1); + + ripple::Dir iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 2); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), ag) == iod.end()); + } + } + } + + void + testIOURippleState(FeatureBitset features) + { + testcase("IOU RippleState"); + using namespace test::jtx; + using namespace std::literals; + + struct TestAccountData + { + Account src; + Account dst; + Account gw; + bool hasTrustline; + bool negative; + }; + + std::array tests = {{ + // src > dst && src > issuer && dst no trustline + {Account("alice2"), Account("bob0"), Account{"gw0"}, false, true}, + // src < dst && src < issuer && dst no trustline + {Account("carol0"), Account("dan1"), Account{"gw1"}, false, false}, + // dst > src && dst > issuer && dst no trustline + {Account("dan1"), Account("alice2"), Account{"gw0"}, false, true}, + // dst < src && dst < issuer && dst no trustline + {Account("bob0"), Account("carol0"), Account{"gw1"}, false, false}, + // src > dst && src > issuer && dst has trustline + {Account("alice2"), Account("bob0"), Account{"gw0"}, true, true}, + // src < dst && src < issuer && dst has trustline + {Account("carol0"), Account("dan1"), Account{"gw1"}, true, false}, + // dst > src && dst > issuer && dst has trustline + {Account("dan1"), Account("alice2"), Account{"gw0"}, true, true}, + // dst < src && dst < issuer && dst has trustline + {Account("bob0"), Account("carol0"), Account{"gw1"}, true, false}, + }}; + + for (auto const& t : tests) + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const USD = t.gw["USD"]; + env.fund(XRP(5000), t.src, t.dst, t.gw); + env(fset(t.gw, asfAllowTrustLineLocking)); + env.close(); + + if (t.hasTrustline) + env.trust(USD(100'000), t.src, t.dst); + else + env.trust(USD(100'000), t.src); + env.close(); + + env(pay(t.gw, t.src, USD(10'000))); + if (t.hasTrustline) + env(pay(t.gw, t.dst, USD(10'000))); + env.close(); + + // src can create escrow + auto const seq1 = env.seq(t.src); + auto const delta = USD(1'000); + env(escrow::create(t.src, t.dst, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // dst can finish escrow + auto const preSrc = env.balance(t.src, USD); + auto const preDst = env.balance(t.dst, USD); + + env(escrow::finish(t.dst, t.src, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + + BEAST_EXPECT(env.balance(t.src, USD) == preSrc); + BEAST_EXPECT(env.balance(t.dst, USD) == preDst + delta); + } + } + + void + testIOUGateway(FeatureBitset features) + { + testcase("IOU Gateway"); + using namespace test::jtx; + using namespace std::literals; + + struct TestAccountData + { + Account src; + Account dst; + bool hasTrustline; + }; + + // issuer is source + { + auto const gw = Account{"gateway"}; + auto const alice = Account{"alice"}; + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(100'000), alice); + env.close(); + + env(pay(gw, alice, USD(10'000))); + env.close(); + + // issuer cannot create escrow + env(escrow::create(gw, alice, USD(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_PERMISSION)); + env.close(); + } + + std::array gwDstTests = {{ + // src > dst && src > issuer && dst has trustline + {Account("alice2"), Account{"gw0"}, true}, + // src < dst && src < issuer && dst has trustline + {Account("carol0"), Account{"gw1"}, true}, + // dst > src && dst > issuer && dst has trustline + {Account("dan1"), Account{"gw0"}, true}, + // dst < src && dst < issuer && dst has trustline + {Account("bob0"), Account{"gw1"}, true}, + }}; + + // issuer is destination + for (auto const& t : gwDstTests) + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const USD = t.dst["USD"]; + env.fund(XRP(5000), t.dst, t.src); + env(fset(t.dst, asfAllowTrustLineLocking)); + env.close(); + + env.trust(USD(100'000), t.src); + env.close(); + + env(pay(t.dst, t.src, USD(10'000))); + env.close(); + + // issuer can receive escrow + auto const seq1 = env.seq(t.src); + auto const preSrc = env.balance(t.src, USD); + env(escrow::create(t.src, t.dst, USD(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // issuer can finish escrow, no dest trustline + env(escrow::finish(t.dst, t.src, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + auto const preAmount = 10'000; + BEAST_EXPECT(preSrc == USD(preAmount)); + auto const postAmount = 9000; + BEAST_EXPECT(env.balance(t.src, USD) == USD(postAmount)); + BEAST_EXPECT(env.balance(t.dst, USD) == USD(0)); + } + + // issuer is source and destination + { + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(5000), gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + + // issuer cannot receive escrow + env(escrow::create(gw, gw, USD(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_PERMISSION)); + env.close(); + } + } + + void + testIOULockedRate(FeatureBitset features) + { + testcase("IOU Locked Rate"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + + // test locked rate + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(rate(gw, 1.25)); + env.close(); + env.trust(USD(100'000), alice); + env.trust(USD(100'000), bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + // alice can create escrow w/ xfer rate + auto const preAlice = env.balance(alice, USD); + auto const seq1 = env.seq(alice); + auto const delta = USD(125); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + auto const transferRate = escrow::rate(env, alice, seq1); + BEAST_EXPECT( + transferRate.value == std::uint32_t(1'000'000'000 * 1.25)); + + // bob can finish escrow + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + + BEAST_EXPECT(env.balance(alice, USD) == preAlice - delta); + BEAST_EXPECT(env.balance(bob, USD) == USD(10'100)); + } + // test rate change - higher + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(rate(gw, 1.25)); + env.close(); + env.trust(USD(100'000), alice); + env.trust(USD(100'000), bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + // alice can create escrow w/ xfer rate + auto const preAlice = env.balance(alice, USD); + auto const seq1 = env.seq(alice); + auto const delta = USD(125); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + auto transferRate = escrow::rate(env, alice, seq1); + BEAST_EXPECT( + transferRate.value == std::uint32_t(1'000'000'000 * 1.25)); + + // issuer changes rate higher + env(rate(gw, 1.26)); + env.close(); + + // bob can finish escrow - rate unchanged + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + + BEAST_EXPECT(env.balance(alice, USD) == preAlice - delta); + BEAST_EXPECT(env.balance(bob, USD) == USD(10'100)); + } + + // test rate change - lower + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(rate(gw, 1.25)); + env.close(); + env.trust(USD(100'000), alice); + env.trust(USD(100'000), bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + // alice can create escrow w/ xfer rate + auto const preAlice = env.balance(alice, USD); + auto const seq1 = env.seq(alice); + auto const delta = USD(125); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + auto transferRate = escrow::rate(env, alice, seq1); + BEAST_EXPECT( + transferRate.value == std::uint32_t(1'000'000'000 * 1.25)); + + // issuer changes rate lower + env(rate(gw, 1.00)); + env.close(); + + // bob can finish escrow - rate changed + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + + BEAST_EXPECT(env.balance(alice, USD) == preAlice - delta); + BEAST_EXPECT(env.balance(bob, USD) == USD(10125)); + } + + // test cancel doesnt charge rate + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(rate(gw, 1.25)); + env.close(); + env.trust(USD(100'000), alice); + env.trust(USD(100'000), bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + // alice can create escrow w/ xfer rate + auto const preAlice = env.balance(alice, USD); + auto const seq1 = env.seq(alice); + auto const delta = USD(125); + env(escrow::create(alice, bob, delta), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 3s), + fee(baseFee)); + env.close(); + auto transferRate = escrow::rate(env, alice, seq1); + BEAST_EXPECT( + transferRate.value == std::uint32_t(1'000'000'000 * 1.25)); + + // issuer changes rate lower + env(rate(gw, 1.00)); + env.close(); + + // alice can cancel escrow - rate is not charged + env(escrow::cancel(alice, alice, seq1), fee(baseFee)); + env.close(); + + BEAST_EXPECT(env.balance(alice, USD) == preAlice); + BEAST_EXPECT(env.balance(bob, USD) == USD(10000)); + } + } + + void + testIOULimitAmount(FeatureBitset features) + { + testcase("IOU Limit"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + + // test LimitAmount + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(1'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice, bob); + env.close(); + env(pay(gw, alice, USD(1'000))); + env(pay(gw, bob, USD(1'000))); + env.close(); + + // alice can create escrow + auto seq1 = env.seq(alice); + auto const delta = USD(125); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // bob can finish + auto const preBobLimit = env.limit(bob, USD); + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + auto const postBobLimit = env.limit(bob, USD); + // bobs limit is NOT changed + BEAST_EXPECT(postBobLimit == preBobLimit); + } + } + + void + testIOURequireAuth(FeatureBitset features) + { + testcase("IOU Require Auth"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + + auto const aliceUSD = alice["USD"]; + auto const bobUSD = bob["USD"]; + + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(1'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(fset(gw, asfRequireAuth)); + env.close(); + env(trust(gw, aliceUSD(10'000)), txflags(tfSetfAuth)); + env(trust(alice, USD(10'000))); + env(trust(bob, USD(10'000))); + env.close(); + env(pay(gw, alice, USD(1'000))); + env.close(); + + // alice cannot create escrow - fails without auth + auto seq1 = env.seq(alice); + auto const delta = USD(125); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_AUTH)); + env.close(); + + // set auth on bob + env(trust(gw, bobUSD(10'000)), txflags(tfSetfAuth)); + env(trust(bob, USD(10'000))); + env.close(); + env(pay(gw, bob, USD(1'000))); + env.close(); + + // alice can create escrow - bob has auth + seq1 = env.seq(alice); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // bob can finish + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + } + + void + testIOUFreeze(FeatureBitset features) + { + testcase("IOU Freeze"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + + // test Global Freeze + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(100'000), alice); + env.trust(USD(100'000), bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + env(fset(gw, asfGlobalFreeze)); + env.close(); + + // setup transaction + auto seq1 = env.seq(alice); + auto const delta = USD(125); + + // create escrow fails - frozen trustline + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecFROZEN)); + env.close(); + + // clear global freeze + env(fclear(gw, asfGlobalFreeze)); + env.close(); + + // create escrow success + seq1 = env.seq(alice); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // set global freeze + env(fset(gw, asfGlobalFreeze)); + env.close(); + + // bob finish escrow success regardless of frozen assets + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + + // clear global freeze + env(fclear(gw, asfGlobalFreeze)); + env.close(); + + // create escrow success + seq1 = env.seq(alice); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::cancel_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // set global freeze + env(fset(gw, asfGlobalFreeze)); + env.close(); + + // bob cancel escrow success regardless of frozen assets + env(escrow::cancel(bob, alice, seq1), fee(baseFee)); + env.close(); + } + + // test Individual Freeze + { + // Env Setup + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env(trust(alice, USD(100'000))); + env(trust(bob, USD(100'000))); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + // set freeze on alice trustline + env(trust(gw, USD(10'000), alice, tfSetFreeze)); + env.close(); + + // setup transaction + auto seq1 = env.seq(alice); + auto const delta = USD(125); + + // create escrow fails - frozen trustline + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecFROZEN)); + env.close(); + + // clear freeze on alice trustline + env(trust(gw, USD(10'000), alice, tfClearFreeze)); + env.close(); + + // create escrow success + seq1 = env.seq(alice); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // set freeze on bob trustline + env(trust(gw, USD(10'000), bob, tfSetFreeze)); + env.close(); + + // bob finish escrow success regardless of frozen assets + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + + // reset freeze on bob and alice trustline + env(trust(gw, USD(10'000), alice, tfClearFreeze)); + env(trust(gw, USD(10'000), bob, tfClearFreeze)); + env.close(); + + // create escrow success + seq1 = env.seq(alice); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::cancel_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // set freeze on bob trustline + env(trust(gw, USD(10'000), bob, tfSetFreeze)); + env.close(); + + // bob cancel escrow success regardless of frozen assets + env(escrow::cancel(bob, alice, seq1), fee(baseFee)); + env.close(); + } + + // test Deep Freeze + { + // Env Setup + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env(trust(alice, USD(100'000))); + env(trust(bob, USD(100'000))); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + // set freeze on alice trustline + env(trust(gw, USD(10'000), alice, tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + // setup transaction + auto seq1 = env.seq(alice); + auto const delta = USD(125); + + // create escrow fails - frozen trustline + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecFROZEN)); + env.close(); + + // clear freeze on alice trustline + env(trust( + gw, USD(10'000), alice, tfClearFreeze | tfClearDeepFreeze)); + env.close(); + + // create escrow success + seq1 = env.seq(alice); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // set freeze on bob trustline + env(trust(gw, USD(10'000), bob, tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + // bob finish escrow fails because of deep frozen assets + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tecFROZEN)); + env.close(); + + // reset freeze on alice and bob trustline + env(trust( + gw, USD(10'000), alice, tfClearFreeze | tfClearDeepFreeze)); + env(trust(gw, USD(10'000), bob, tfClearFreeze | tfClearDeepFreeze)); + env.close(); + + // create escrow success + seq1 = env.seq(alice); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::cancel_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // set freeze on bob trustline + env(trust(gw, USD(10'000), bob, tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + // bob cancel escrow fails because of deep frozen assets + env(escrow::cancel(bob, alice, seq1), + fee(baseFee), + ter(tesSUCCESS)); + env.close(); + } + } + void + testIOUINSF(FeatureBitset features) + { + testcase("IOU Insuficient Funds"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + { + // test tecPATH_PARTIAL + // ie. has 10'000, escrow 1'000 then try to pay 10'000 + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(100'000), alice); + env.trust(USD(100'000), bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + // create escrow success + auto const delta = USD(1'000); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + env(pay(alice, gw, USD(10'000)), ter(tecPATH_PARTIAL)); + } + { + // test tecINSUFFICIENT_FUNDS + // ie. has 10'000 escrow 1'000 then try to escrow 10'000 + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(100'000), alice); + env.trust(USD(100'000), bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + auto const delta = USD(1'000); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + env(escrow::create(alice, bob, USD(10'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecINSUFFICIENT_FUNDS)); + env.close(); + } + } + + void + testIOUPrecisionLoss(FeatureBitset features) + { + testcase("IOU Precision Loss"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + + // test min create precision loss + { + Env env(*this, features); + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(100000000000000000), alice); + env.trust(USD(100000000000000000), bob); + env.close(); + env(pay(gw, alice, USD(10000000000000000))); + env(pay(gw, bob, USD(1))); + env.close(); + + // alice cannot create escrow for 1/10 iou - precision loss + env(escrow::create(alice, bob, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecPRECISION_LOSS)); + env.close(); + + auto const seq1 = env.seq(alice); + // alice can create escrow for 1'000 iou + env(escrow::create(alice, bob, USD(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // bob finish escrow success + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + } + } + + void + testMPTEnablement(FeatureBitset features) + { + testcase("MPT Enablement"); + + using namespace jtx; + using namespace std::chrono; + + for (bool const withTokenEscrow : {false, true}) + { + auto const amend = + withTokenEscrow ? features : features - featureTokenEscrow; + Env env{*this, amend}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + env.fund(XRP(5000), bob); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env.close(); + + auto const createResult = + withTokenEscrow ? ter(tesSUCCESS) : ter(temBAD_AMOUNT); + auto const finishResult = + withTokenEscrow ? ter(tesSUCCESS) : ter(tecNO_TARGET); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, MPT(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + createResult); + env.close(); + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + finishResult); + env.close(); + auto const seq2 = env.seq(alice); + env(escrow::create(alice, bob, MPT(1'000)), + escrow::condition(escrow::cb2), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s), + fee(baseFee * 150), + createResult); + env.close(); + env(escrow::cancel(bob, alice, seq2), finishResult); + env.close(); + } + } + + void + testMPTCreatePreflight(FeatureBitset features) + { + testcase("MPT Create Preflight"); + using namespace test::jtx; + using namespace std::literals; + + for (bool const withMPT : {true, false}) + { + auto const amend = + withMPT ? features : features - featureMPTokensV1; + Env env{*this, amend}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + env.fund(XRP(1'000), alice, bob, gw); + + Json::Value jv = escrow::create(alice, bob, XRP(1)); + jv.removeMember(jss::Amount); + jv[jss::Amount][jss::mpt_issuance_id] = + "00000004A407AF5856CCF3C42619DAA925813FC955C72983"; + jv[jss::Amount][jss::value] = "-1"; + + auto const result = withMPT ? ter(temBAD_AMOUNT) : ter(temDISABLED); + env(jv, + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + result); + env.close(); + } + + // temBAD_AMOUNT: amount < 0 + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + env(escrow::create(alice, bob, MPT(-1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(temBAD_AMOUNT)); + env.close(); + } + } + + void + testMPTCreatePreclaim(FeatureBitset features) + { + testcase("MPT Create Preclaim"); + using namespace test::jtx; + using namespace std::literals; + + // tecNO_PERMISSION: issuer is the same as the account + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env.close(); + + env(escrow::create(gw, alice, MPT(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_PERMISSION)); + env.close(); + } + + // tecOBJECT_NOT_FOUND: mpt does not exist + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + env.fund(XRP(10'000), alice, bob, gw); + env.close(); + + auto const mpt = ripple::test::jtx::MPT( + alice.name(), makeMptID(env.seq(alice), alice)); + Json::Value jv = escrow::create(alice, bob, mpt(2)); + jv[jss::Amount][jss::mpt_issuance_id] = + "00000004A407AF5856CCF3C42619DAA925813FC955C72983"; + env(jv, + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecOBJECT_NOT_FOUND)); + env.close(); + } + + // tecNO_PERMISSION: tfMPTCanEscrow is not enabled + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + env(escrow::create(alice, bob, MPT(3)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_PERMISSION)); + env.close(); + } + + // tecOBJECT_NOT_FOUND: account does not have the mpt + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + auto const MPT = mptGw["MPT"]; + + env(escrow::create(alice, bob, MPT(4)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecOBJECT_NOT_FOUND)); + env.close(); + } + + // tecNO_AUTH: requireAuth set: account not authorized + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = + tfMPTCanEscrow | tfMPTCanTransfer | tfMPTRequireAuth}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = gw, .holder = alice}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env.close(); + + // unauthorize account + mptGw.authorize( + {.account = gw, .holder = alice, .flags = tfMPTUnauthorize}); + + env(escrow::create(alice, bob, MPT(5)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_AUTH)); + env.close(); + } + + // tecNO_AUTH: requireAuth set: dest not authorized + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = + tfMPTCanEscrow | tfMPTCanTransfer | tfMPTRequireAuth}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = gw, .holder = alice}); + mptGw.authorize({.account = bob}); + mptGw.authorize({.account = gw, .holder = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + // unauthorize dest + mptGw.authorize( + {.account = gw, .holder = bob, .flags = tfMPTUnauthorize}); + + env(escrow::create(alice, bob, MPT(6)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_AUTH)); + env.close(); + } + + // tecLOCKED: issuer has locked the account + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTCanLock}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + // lock account + mptGw.set({.account = gw, .holder = alice, .flags = tfMPTLock}); + + env(escrow::create(alice, bob, MPT(7)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecLOCKED)); + env.close(); + } + + // tecLOCKED: issuer has locked the dest + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTCanLock}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + // lock dest + mptGw.set({.account = gw, .holder = bob, .flags = tfMPTLock}); + + env(escrow::create(alice, bob, MPT(8)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecLOCKED)); + env.close(); + } + + // tecNO_AUTH: mpt cannot be transferred + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + env(escrow::create(alice, bob, MPT(9)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_AUTH)); + env.close(); + } + + // tecINSUFFICIENT_FUNDS: spendable amount is zero + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, bob, MPT(10))); + env.close(); + + env(escrow::create(alice, bob, MPT(11)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecINSUFFICIENT_FUNDS)); + env.close(); + } + + // tecINSUFFICIENT_FUNDS: spendable amount is less than the amount + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10))); + env(pay(gw, bob, MPT(10))); + env.close(); + + env(escrow::create(alice, bob, MPT(11)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecINSUFFICIENT_FUNDS)); + env.close(); + } + } + + void + testMPTFinishPreclaim(FeatureBitset features) + { + testcase("MPT Finish Preclaim"); + using namespace test::jtx; + using namespace std::literals; + + // tecNO_AUTH: requireAuth set: dest not authorized + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = + tfMPTCanEscrow | tfMPTCanTransfer | tfMPTRequireAuth}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = gw, .holder = alice}); + mptGw.authorize({.account = bob}); + mptGw.authorize({.account = gw, .holder = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, MPT(10)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + // unauthorize dest + mptGw.authorize( + {.account = gw, .holder = bob, .flags = tfMPTUnauthorize}); + + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tecNO_AUTH)); + env.close(); + } + + // tecOBJECT_NOT_FOUND: MPT issuance does not exist + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10'000), alice, bob); + env.close(); + + auto const seq1 = env.seq(alice); + env.app().openLedger().modify( + [&](OpenView& view, beast::Journal j) { + Sandbox sb(&view, tapNONE); + auto sleNew = + std::make_shared(keylet::escrow(alice, seq1)); + MPTIssue const mpt{ + MPTIssue{makeMptID(1, AccountID(0x4985601))}}; + STAmount amt(mpt, 10); + sleNew->setAccountID(sfDestination, bob); + sleNew->setFieldAmount(sfAmount, amt); + sb.insert(sleNew); + sb.apply(view); + return true; + }); + + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tecOBJECT_NOT_FOUND)); + env.close(); + } + + // tecLOCKED: issuer has locked the dest + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTCanLock}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, MPT(8)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + // lock dest + mptGw.set({.account = gw, .holder = bob, .flags = tfMPTLock}); + + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tecLOCKED)); + env.close(); + } + } + + void + testMPTFinishDoApply(FeatureBitset features) + { + testcase("MPT Finish Do Apply"); + using namespace test::jtx; + using namespace std::literals; + + // tecINSUFFICIENT_RESERVE: insufficient reserve to create MPT + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const acctReserve = env.current()->fees().accountReserve(0); + auto const incReserve = env.current()->fees().increment; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + env.fund(acctReserve + (incReserve - 1), bob); + env.close(); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, MPT(10)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tecINSUFFICIENT_RESERVE)); + env.close(); + } + + // tesSUCCESS: bob submits; finish MPT created + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + env.fund(XRP(10'000), bob); + env.close(); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, MPT(10)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + } + + // tecNO_PERMISSION: carol submits; finish MPT not created + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account("gw"); + env.fund(XRP(10'000), bob, carol); + env.close(); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, MPT(10)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + env(escrow::finish(carol, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tecNO_PERMISSION)); + env.close(); + } + } + + void + testMPTCancelPreclaim(FeatureBitset features) + { + testcase("MPT Cancel Preclaim"); + using namespace test::jtx; + using namespace std::literals; + + // tecNO_AUTH: requireAuth set: account not authorized + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = + tfMPTCanEscrow | tfMPTCanTransfer | tfMPTRequireAuth}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = gw, .holder = alice}); + mptGw.authorize({.account = bob}); + mptGw.authorize({.account = gw, .holder = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, MPT(10)), + escrow::cancel_time(env.now() + 2s), + escrow::condition(escrow::cb1), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + // unauthorize account + mptGw.authorize( + {.account = gw, .holder = alice, .flags = tfMPTUnauthorize}); + + env(escrow::cancel(bob, alice, seq1), ter(tecNO_AUTH)); + env.close(); + } + + // tecOBJECT_NOT_FOUND: MPT issuance does not exist + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10'000), alice, bob); + + auto const seq1 = env.seq(alice); + env.app().openLedger().modify( + [&](OpenView& view, beast::Journal j) { + Sandbox sb(&view, tapNONE); + auto sleNew = + std::make_shared(keylet::escrow(alice, seq1)); + MPTIssue const mpt{ + MPTIssue{makeMptID(1, AccountID(0x4985601))}}; + STAmount amt(mpt, 10); + sleNew->setAccountID(sfDestination, bob); + sleNew->setFieldAmount(sfAmount, amt); + sb.insert(sleNew); + sb.apply(view); + return true; + }); + + env(escrow::cancel(bob, alice, seq1), + fee(baseFee), + ter(tecOBJECT_NOT_FOUND)); + env.close(); + } + } + + void + testMPTBalances(FeatureBitset features) + { + testcase("MPT Balances"); + + using namespace jtx; + using namespace std::chrono; + + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account("gw"); + env.fund(XRP(5000), bob); + + MPTTester mptGw(env, gw, {.holders = {alice, carol}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = carol}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, carol, MPT(10'000))); + env.close(); + + auto outstandingMPT = env.balance(gw, MPT); + + // Create & Finish Escrow + auto const seq1 = env.seq(alice); + { + auto const preAliceMPT = env.balance(alice, MPT); + auto const preBobMPT = env.balance(bob, MPT); + env(escrow::create(alice, bob, MPT(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1'000)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 1'000); + BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 1'000); + } + { + auto const preAliceMPT = env.balance(alice, MPT); + auto const preBobMPT = env.balance(bob, MPT); + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); + BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT + MPT(1'000)); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0); + } + + // Create & Cancel Escrow + auto const seq2 = env.seq(alice); + { + auto const preAliceMPT = env.balance(alice, MPT); + auto const preBobMPT = env.balance(bob, MPT); + env(escrow::create(alice, bob, MPT(1'000)), + escrow::condition(escrow::cb2), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1'000)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 1'000); + BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 1'000); + } + { + auto const preAliceMPT = env.balance(alice, MPT); + auto const preBobMPT = env.balance(bob, MPT); + env(escrow::cancel(bob, alice, seq2), ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT + MPT(1'000)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); + BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0); + } + + // Self Escrow Create & Finish + { + auto const seq = env.seq(alice); + auto const preAliceMPT = env.balance(alice, MPT); + env(escrow::create(alice, alice, MPT(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1'000)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 1'000); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 1'000); + + env(escrow::finish(alice, alice, seq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0); + } + + // Self Escrow Create & Cancel + { + auto const seq = env.seq(alice); + auto const preAliceMPT = env.balance(alice, MPT); + env(escrow::create(alice, alice, MPT(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1'000)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 1'000); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 1'000); + + env(escrow::cancel(alice, alice, seq), ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0); + } + + // Multiple Escrows + { + auto const preAliceMPT = env.balance(alice, MPT); + auto const preBobMPT = env.balance(bob, MPT); + auto const preCarolMPT = env.balance(carol, MPT); + env(escrow::create(alice, bob, MPT(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + env(escrow::create(carol, bob, MPT(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1'000)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 1'000); + BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + BEAST_EXPECT(env.balance(carol, MPT) == preCarolMPT - MPT(1'000)); + BEAST_EXPECT(mptEscrowed(env, carol, MPT) == 1'000); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 2'000); + } + + // Max MPT Amount Issued (Escrow 1 MPT) + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(maxMPTokenAmount))); + env.close(); + + auto const preAliceMPT = env.balance(alice, MPT); + auto const preBobMPT = env.balance(bob, MPT); + auto const outstandingMPT = env.balance(gw, MPT); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, MPT(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 1); + BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 1); + + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); + BEAST_EXPECT(!env.le(keylet::mptoken(MPT.mpt(), alice)) + ->isFieldPresent(sfLockedAmount)); + BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT + MPT(1)); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0); + BEAST_EXPECT(!env.le(keylet::mptIssuance(MPT.mpt())) + ->isFieldPresent(sfLockedAmount)); + } + + // Max MPT Amount Issued (Escrow Max MPT) + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(maxMPTokenAmount))); + env.close(); + + auto const preAliceMPT = env.balance(alice, MPT); + auto const preBobMPT = env.balance(bob, MPT); + auto const outstandingMPT = env.balance(gw, MPT); + + // Escrow Max MPT - 10 + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, MPT(maxMPTokenAmount - 10)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // Escrow 10 MPT + auto const seq2 = env.seq(alice); + env(escrow::create(alice, bob, MPT(10)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + BEAST_EXPECT( + env.balance(alice, MPT) == preAliceMPT - MPT(maxMPTokenAmount)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == maxMPTokenAmount); + BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == maxMPTokenAmount); + + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + env(escrow::finish(bob, alice, seq2), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT( + env.balance(alice, MPT) == preAliceMPT - MPT(maxMPTokenAmount)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); + BEAST_EXPECT( + env.balance(bob, MPT) == preBobMPT + MPT(maxMPTokenAmount)); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0); + } + } + + void + testMPTMetaAndOwnership(FeatureBitset features) + { + using namespace jtx; + using namespace std::chrono; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account{"gateway"}; + { + testcase("MPT Metadata to self"); + + Env env{*this, features}; + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + auto const aseq = env.seq(alice); + auto const bseq = env.seq(bob); + + env(escrow::create(alice, alice, MPT(1'000)), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 500s)); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + env.close(5s); + auto const aa = env.le(keylet::escrow(alice.id(), aseq)); + BEAST_EXPECT(aa); + { + ripple::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 2); + BEAST_EXPECT( + std::find(aod.begin(), aod.end(), aa) != aod.end()); + } + + { + ripple::Dir iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 1); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), aa) == iod.end()); + } + + env(escrow::create(bob, bob, MPT(1'000)), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s)); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + env.close(5s); + auto const bb = env.le(keylet::escrow(bob.id(), bseq)); + BEAST_EXPECT(bb); + + { + ripple::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 2); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), bb) != bod.end()); + } + + env.close(5s); + env(escrow::finish(alice, alice, aseq)); + { + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + + ripple::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); + BEAST_EXPECT( + std::find(aod.begin(), aod.end(), aa) == aod.end()); + + ripple::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 2); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), bb) != bod.end()); + } + + env.close(5s); + env(escrow::cancel(bob, bob, bseq)); + { + BEAST_EXPECT(!env.le(keylet::escrow(bob.id(), bseq))); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + + ripple::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 1); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), bb) == bod.end()); + } + } + + { + testcase("MPT Metadata to other"); + + Env env{*this, features}; + MPTTester mptGw(env, gw, {.holders = {alice, bob, carol}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + mptGw.authorize({.account = carol}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env(pay(gw, carol, MPT(10'000))); + env.close(); + auto const aseq = env.seq(alice); + auto const bseq = env.seq(bob); + + env(escrow::create(alice, bob, MPT(1'000)), + escrow::finish_time(env.now() + 1s)); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + env.close(5s); + env(escrow::create(bob, carol, MPT(1'000)), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s)); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + env.close(5s); + + auto const ab = env.le(keylet::escrow(alice.id(), aseq)); + BEAST_EXPECT(ab); + + auto const bc = env.le(keylet::escrow(bob.id(), bseq)); + BEAST_EXPECT(bc); + + { + ripple::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 2); + BEAST_EXPECT( + std::find(aod.begin(), aod.end(), ab) != aod.end()); + + ripple::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 3); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), ab) != bod.end()); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), bc) != bod.end()); + + ripple::Dir cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 2); + BEAST_EXPECT( + std::find(cod.begin(), cod.end(), bc) != cod.end()); + } + + env.close(5s); + env(escrow::finish(alice, alice, aseq)); + { + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); + BEAST_EXPECT(env.le(keylet::escrow(bob.id(), bseq))); + + ripple::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); + BEAST_EXPECT( + std::find(aod.begin(), aod.end(), ab) == aod.end()); + + ripple::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 2); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), ab) == bod.end()); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), bc) != bod.end()); + + ripple::Dir cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 2); + } + + env.close(5s); + env(escrow::cancel(bob, bob, bseq)); + { + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); + BEAST_EXPECT(!env.le(keylet::escrow(bob.id(), bseq))); + + ripple::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); + BEAST_EXPECT( + std::find(aod.begin(), aod.end(), ab) == aod.end()); + + ripple::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 1); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), ab) == bod.end()); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), bc) == bod.end()); + + ripple::Dir cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 1); + } + } + } + + void + testMPTGateway(FeatureBitset features) + { + testcase("MPT Gateway Balances"); + using namespace test::jtx; + using namespace std::literals; + + // issuer is dest; alice w/ authorization + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env.close(); + + // issuer can be destination + auto const seq1 = env.seq(alice); + auto const preAliceMPT = env.balance(alice, MPT); + auto const preOutstanding = env.balance(gw, MPT); + auto const preEscrowed = issuerMPTEscrowed(env, MPT); + BEAST_EXPECT(preOutstanding == MPT(10'000)); + BEAST_EXPECT(preEscrowed == 0); + + env(escrow::create(alice, gw, MPT(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1'000)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 1'000); + BEAST_EXPECT(env.balance(gw, MPT) == preOutstanding); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == preEscrowed + 1'000); + + // issuer (dest) can finish escrow + env(escrow::finish(gw, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1'000)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == preOutstanding - MPT(1'000)); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == preEscrowed); + } + } + + void + testMPTLockedRate(FeatureBitset features) + { + testcase("MPT Locked Rate"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + + // test locked rate: finish + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.transferFee = 25000, + .ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + // alice can create escrow w/ xfer rate + auto const preAlice = env.balance(alice, MPT); + auto const seq1 = env.seq(alice); + auto const delta = MPT(125); + env(escrow::create(alice, bob, MPT(125)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + auto const transferRate = escrow::rate(env, alice, seq1); + BEAST_EXPECT( + transferRate.value == std::uint32_t(1'000'000'000 * 1.25)); + + // bob can finish escrow + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAlice - delta); + BEAST_EXPECT(env.balance(bob, MPT) == MPT(10'100)); + } + + // test locked rate: cancel + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.transferFee = 25000, + .ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + // alice can create escrow w/ xfer rate + auto const preAlice = env.balance(alice, MPT); + auto const preBob = env.balance(bob, MPT); + auto const seq1 = env.seq(alice); + auto const delta = MPT(125); + env(escrow::create(alice, bob, MPT(125)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 3s), + fee(baseFee * 150)); + env.close(); + auto const transferRate = escrow::rate(env, alice, seq1); + BEAST_EXPECT( + transferRate.value == std::uint32_t(1'000'000'000 * 1.25)); + + // alice can cancel escrow + env(escrow::cancel(alice, alice, seq1), fee(baseFee)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAlice); + BEAST_EXPECT(env.balance(bob, MPT) == preBob); + } + } + + void + testMPTRequireAuth(FeatureBitset features) + { + testcase("MPT Require Auth"); + using namespace test::jtx; + using namespace std::literals; + + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTRequireAuth}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = gw, .holder = alice}); + mptGw.authorize({.account = bob}); + mptGw.authorize({.account = gw, .holder = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env.close(); + + auto seq = env.seq(alice); + auto const delta = MPT(125); + // alice can create escrow - is authorized + env(escrow::create(alice, bob, MPT(100)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // bob can finish escrow - is authorized + env(escrow::finish(bob, alice, seq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + } + + void + testMPTLock(FeatureBitset features) + { + testcase("MPT Lock"); + using namespace test::jtx; + using namespace std::literals; + + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTCanLock}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + // alice create escrow + auto seq1 = env.seq(alice); + env(escrow::create(alice, bob, MPT(100)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s), + fee(baseFee * 150)); + env.close(); + + // lock account & dest + mptGw.set({.account = gw, .holder = alice, .flags = tfMPTLock}); + mptGw.set({.account = gw, .holder = bob, .flags = tfMPTLock}); + + // bob cannot finish + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tecLOCKED)); + env.close(); + + // bob can cancel + env(escrow::cancel(bob, alice, seq1)); + env.close(); + } + + void + testMPTCanTransfer(FeatureBitset features) + { + testcase("MPT Can Transfer"); + using namespace test::jtx; + using namespace std::literals; + + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + // alice cannot create escrow to non issuer + env(escrow::create(alice, bob, MPT(100)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s), + fee(baseFee * 150), + ter(tecNO_AUTH)); + env.close(); + + // Escrow Create & Finish + { + // alice an create escrow to issuer + auto seq = env.seq(alice); + env(escrow::create(alice, gw, MPT(100)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // gw can finish + env(escrow::finish(gw, alice, seq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + } + + // Escrow Create & Cancel + { + // alice an create escrow to issuer + auto seq = env.seq(alice); + env(escrow::create(alice, gw, MPT(100)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s), + fee(baseFee * 150)); + env.close(); + + // alice can cancel + env(escrow::cancel(alice, alice, seq)); + env.close(); + } + } + + void + testMPTDestroy(FeatureBitset features) + { + testcase("MPT Destroy"); + using namespace test::jtx; + using namespace std::literals; + + // tecHAS_OBLIGATIONS: issuer cannot destroy issuance + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, MPT(10)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + env(pay(alice, gw, MPT(10'000)), ter(tecPATH_PARTIAL)); + env(pay(alice, gw, MPT(9'990))); + env(pay(bob, gw, MPT(10'000))); + BEAST_EXPECT(env.balance(alice, MPT) == MPT(0)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 10); + BEAST_EXPECT(env.balance(bob, MPT) == MPT(0)); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == MPT(10)); + mptGw.authorize({.account = bob, .flags = tfMPTUnauthorize}); + mptGw.destroy( + {.id = mptGw.issuanceID(), + .ownerCount = 1, + .err = tecHAS_OBLIGATIONS}); + + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + env(pay(bob, gw, MPT(10))); + mptGw.destroy({.id = mptGw.issuanceID(), .ownerCount = 0}); + } + + // tecHAS_OBLIGATIONS: holder cannot destroy mptoken + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + env.fund(XRP(10'000), bob); + env.close(); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, MPT(10)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + env(pay(alice, gw, MPT(9'990))); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == MPT(0)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 10); + mptGw.authorize( + {.account = alice, + .flags = tfMPTUnauthorize, + .err = tecHAS_OBLIGATIONS}); + + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == MPT(0)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); + mptGw.authorize({.account = alice, .flags = tfMPTUnauthorize}); + BEAST_EXPECT(!env.le(keylet::mptoken(MPT.mpt(), alice))); + } + } + + void + testIOUWithFeats(FeatureBitset features) + { + testIOUEnablement(features); + testIOUAllowLockingFlag(features); + testIOUCreatePreflight(features); + testIOUCreatePreclaim(features); + testIOUFinishPreclaim(features); + testIOUFinishDoApply(features); + testIOUCancelPreclaim(features); + testIOUBalances(features); + testIOUMetaAndOwnership(features); + testIOURippleState(features); + testIOUGateway(features); + testIOULockedRate(features); + testIOULimitAmount(features); + testIOURequireAuth(features); + testIOUFreeze(features); + testIOUINSF(features); + testIOUPrecisionLoss(features); + } + + void + testMPTWithFeats(FeatureBitset features) + { + testMPTEnablement(features); + testMPTCreatePreflight(features); + testMPTCreatePreclaim(features); + testMPTFinishPreclaim(features); + testMPTFinishDoApply(features); + testMPTCancelPreclaim(features); + testMPTBalances(features); + testMPTMetaAndOwnership(features); + testMPTGateway(features); + testMPTLockedRate(features); + testMPTRequireAuth(features); + testMPTLock(features); + testMPTCanTransfer(features); + testMPTDestroy(features); + } + +public: + void + run() override + { + using namespace test::jtx; + FeatureBitset const all{testable_amendments()}; + testIOUWithFeats(all); + testMPTWithFeats(all); + } +}; + +BEAST_DEFINE_TESTSUITE(EscrowToken, app, ripple); + +} // namespace test +} // namespace ripple diff --git a/src/test/app/Escrow_test.cpp b/src/test/app/Escrow_test.cpp index 1129019aab..3eaf0f13ea 100644 --- a/src/test/app/Escrow_test.cpp +++ b/src/test/app/Escrow_test.cpp @@ -35,81 +35,53 @@ namespace test { struct Escrow_test : public beast::unit_test::suite { - // A PreimageSha256 fulfillments and its associated condition. - std::array const fb1 = {{0xA0, 0x02, 0x80, 0x00}}; - - std::array const cb1 = { - {0xA0, 0x25, 0x80, 0x20, 0xE3, 0xB0, 0xC4, 0x42, 0x98, 0xFC, - 0x1C, 0x14, 0x9A, 0xFB, 0xF4, 0xC8, 0x99, 0x6F, 0xB9, 0x24, - 0x27, 0xAE, 0x41, 0xE4, 0x64, 0x9B, 0x93, 0x4C, 0xA4, 0x95, - 0x99, 0x1B, 0x78, 0x52, 0xB8, 0x55, 0x81, 0x01, 0x00}}; - - // Another PreimageSha256 fulfillments and its associated condition. - std::array const fb2 = { - {0xA0, 0x05, 0x80, 0x03, 0x61, 0x61, 0x61}}; - - std::array const cb2 = { - {0xA0, 0x25, 0x80, 0x20, 0x98, 0x34, 0x87, 0x6D, 0xCF, 0xB0, - 0x5C, 0xB1, 0x67, 0xA5, 0xC2, 0x49, 0x53, 0xEB, 0xA5, 0x8C, - 0x4A, 0xC8, 0x9B, 0x1A, 0xDF, 0x57, 0xF2, 0x8F, 0x2F, 0x9D, - 0x09, 0xAF, 0x10, 0x7E, 0xE8, 0xF0, 0x81, 0x01, 0x03}}; - - // Another PreimageSha256 fulfillment and its associated condition. - std::array const fb3 = { - {0xA0, 0x06, 0x80, 0x04, 0x6E, 0x69, 0x6B, 0x62}}; - - std::array const cb3 = { - {0xA0, 0x25, 0x80, 0x20, 0x6E, 0x4C, 0x71, 0x45, 0x30, 0xC0, - 0xA4, 0x26, 0x8B, 0x3F, 0xA6, 0x3B, 0x1B, 0x60, 0x6F, 0x2D, - 0x26, 0x4A, 0x2D, 0x85, 0x7B, 0xE8, 0xA0, 0x9C, 0x1D, 0xFD, - 0x57, 0x0D, 0x15, 0x85, 0x8B, 0xD4, 0x81, 0x01, 0x04}}; - void - testEnablement() + testEnablement(FeatureBitset features) { testcase("Enablement"); using namespace jtx; using namespace std::chrono; - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob"); - env(escrow("alice", "bob", XRP(1000)), finish_time(env.now() + 1s)); + env(escrow::create("alice", "bob", XRP(1000)), + escrow::finish_time(env.now() + 1s)); env.close(); auto const seq1 = env.seq("alice"); - env(escrow("alice", "bob", XRP(1000)), - condition(cb1), - finish_time(env.now() + 1s), + env(escrow::create("alice", "bob", XRP(1000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), fee(baseFee * 150)); env.close(); - env(finish("bob", "alice", seq1), - condition(cb1), - fulfillment(fb1), + env(escrow::finish("bob", "alice", seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), fee(baseFee * 150)); auto const seq2 = env.seq("alice"); - env(escrow("alice", "bob", XRP(1000)), - condition(cb2), - finish_time(env.now() + 1s), - cancel_time(env.now() + 2s), + env(escrow::create("alice", "bob", XRP(1000)), + escrow::condition(escrow::cb2), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s), fee(baseFee * 150)); env.close(); - env(cancel("bob", "alice", seq2), fee(baseFee * 150)); + env(escrow::cancel("bob", "alice", seq2), fee(baseFee * 150)); } void - testTiming() + testTiming(FeatureBitset features) { using namespace jtx; using namespace std::chrono; { testcase("Timing: Finish Only"); - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob"); env.close(); @@ -118,21 +90,22 @@ struct Escrow_test : public beast::unit_test::suite auto const ts = env.now() + 97s; auto const seq = env.seq("alice"); - env(escrow("alice", "bob", XRP(1000)), finish_time(ts)); + env(escrow::create("alice", "bob", XRP(1000)), + escrow::finish_time(ts)); // Advance the ledger, verifying that the finish won't complete // prematurely. for (; env.now() < ts; env.close()) - env(finish("bob", "alice", seq), + env(escrow::finish("bob", "alice", seq), fee(baseFee * 150), ter(tecNO_PERMISSION)); - env(finish("bob", "alice", seq), fee(baseFee * 150)); + env(escrow::finish("bob", "alice", seq), fee(baseFee * 150)); } { testcase("Timing: Cancel Only"); - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob"); env.close(); @@ -141,31 +114,31 @@ struct Escrow_test : public beast::unit_test::suite auto const ts = env.now() + 117s; auto const seq = env.seq("alice"); - env(escrow("alice", "bob", XRP(1000)), - condition(cb1), - cancel_time(ts)); + env(escrow::create("alice", "bob", XRP(1000)), + escrow::condition(escrow::cb1), + escrow::cancel_time(ts)); // Advance the ledger, verifying that the cancel won't complete // prematurely. for (; env.now() < ts; env.close()) - env(cancel("bob", "alice", seq), + env(escrow::cancel("bob", "alice", seq), fee(baseFee * 150), ter(tecNO_PERMISSION)); // Verify that a finish won't work anymore. - env(finish("bob", "alice", seq), - condition(cb1), - fulfillment(fb1), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), fee(baseFee * 150), ter(tecNO_PERMISSION)); // Verify that the cancel will succeed - env(cancel("bob", "alice", seq), fee(baseFee * 150)); + env(escrow::cancel("bob", "alice", seq), fee(baseFee * 150)); } { testcase("Timing: Finish and Cancel -> Finish"); - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob"); env.close(); @@ -175,34 +148,34 @@ struct Escrow_test : public beast::unit_test::suite auto const cts = env.now() + 192s; auto const seq = env.seq("alice"); - env(escrow("alice", "bob", XRP(1000)), - finish_time(fts), - cancel_time(cts)); + env(escrow::create("alice", "bob", XRP(1000)), + escrow::finish_time(fts), + escrow::cancel_time(cts)); // Advance the ledger, verifying that the finish and cancel won't // complete prematurely. for (; env.now() < fts; env.close()) { - env(finish("bob", "alice", seq), + env(escrow::finish("bob", "alice", seq), fee(baseFee * 150), ter(tecNO_PERMISSION)); - env(cancel("bob", "alice", seq), + env(escrow::cancel("bob", "alice", seq), fee(baseFee * 150), ter(tecNO_PERMISSION)); } // Verify that a cancel still won't work - env(cancel("bob", "alice", seq), + env(escrow::cancel("bob", "alice", seq), fee(baseFee * 150), ter(tecNO_PERMISSION)); // And verify that a finish will - env(finish("bob", "alice", seq), fee(baseFee * 150)); + env(escrow::finish("bob", "alice", seq), fee(baseFee * 150)); } { testcase("Timing: Finish and Cancel -> Cancel"); - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob"); env.close(); @@ -212,18 +185,18 @@ struct Escrow_test : public beast::unit_test::suite auto const cts = env.now() + 184s; auto const seq = env.seq("alice"); - env(escrow("alice", "bob", XRP(1000)), - finish_time(fts), - cancel_time(cts)); + env(escrow::create("alice", "bob", XRP(1000)), + escrow::finish_time(fts), + escrow::cancel_time(cts)); // Advance the ledger, verifying that the finish and cancel won't // complete prematurely. for (; env.now() < fts; env.close()) { - env(finish("bob", "alice", seq), + env(escrow::finish("bob", "alice", seq), fee(baseFee * 150), ter(tecNO_PERMISSION)); - env(cancel("bob", "alice", seq), + env(escrow::cancel("bob", "alice", seq), fee(baseFee * 150), ter(tecNO_PERMISSION)); } @@ -231,30 +204,30 @@ struct Escrow_test : public beast::unit_test::suite // Continue advancing, verifying that the cancel won't complete // prematurely. At this point a finish would succeed. for (; env.now() < cts; env.close()) - env(cancel("bob", "alice", seq), + env(escrow::cancel("bob", "alice", seq), fee(baseFee * 150), ter(tecNO_PERMISSION)); // Verify that finish will no longer work, since we are past the // cancel activation time. - env(finish("bob", "alice", seq), + env(escrow::finish("bob", "alice", seq), fee(baseFee * 150), ter(tecNO_PERMISSION)); // And verify that a cancel will succeed. - env(cancel("bob", "alice", seq), fee(baseFee * 150)); + env(escrow::cancel("bob", "alice", seq), fee(baseFee * 150)); } } void - testTags() + testTags(FeatureBitset features) { testcase("Tags"); using namespace jtx; using namespace std::chrono; - Env env(*this); + Env env(*this, features); auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -264,15 +237,15 @@ struct Escrow_test : public beast::unit_test::suite // Check to make sure that we correctly detect if tags are really // required: env(fset(bob, asfRequireDest)); - env(escrow(alice, bob, XRP(1000)), - finish_time(env.now() + 1s), + env(escrow::create(alice, bob, XRP(1000)), + escrow::finish_time(env.now() + 1s), ter(tecDST_TAG_NEEDED)); // set source and dest tags auto const seq = env.seq(alice); - env(escrow(alice, bob, XRP(1000)), - finish_time(env.now() + 1s), + env(escrow::create(alice, bob, XRP(1000)), + escrow::finish_time(env.now() + 1s), stag(1), dtag(2)); @@ -283,7 +256,7 @@ struct Escrow_test : public beast::unit_test::suite } void - testDisallowXRP() + testDisallowXRP(FeatureBitset features) { testcase("Disallow XRP"); @@ -292,27 +265,28 @@ struct Escrow_test : public beast::unit_test::suite { // Respect the "asfDisallowXRP" account flag: - Env env(*this, supported_amendments() - featureDepositAuth); + Env env(*this, features - featureDepositAuth); env.fund(XRP(5000), "bob", "george"); env(fset("george", asfDisallowXRP)); - env(escrow("bob", "george", XRP(10)), - finish_time(env.now() + 1s), + env(escrow::create("bob", "george", XRP(10)), + escrow::finish_time(env.now() + 1s), ter(tecNO_TARGET)); } { // Ignore the "asfDisallowXRP" account flag, which we should // have been doing before. - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), "bob", "george"); env(fset("george", asfDisallowXRP)); - env(escrow("bob", "george", XRP(10)), finish_time(env.now() + 1s)); + env(escrow::create("bob", "george", XRP(10)), + escrow::finish_time(env.now() + 1s)); } } void - test1571() + test1571(FeatureBitset features) { using namespace jtx; using namespace std::chrono; @@ -320,7 +294,7 @@ struct Escrow_test : public beast::unit_test::suite { testcase("Implied Finish Time (without fix1571)"); - Env env(*this, supported_amendments() - fix1571); + Env env(*this, testable_amendments() - fix1571); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob", "carol"); env.close(); @@ -328,11 +302,11 @@ struct Escrow_test : public beast::unit_test::suite // Creating an escrow without a finish time and finishing it // is allowed without fix1571: auto const seq1 = env.seq("alice"); - env(escrow("alice", "bob", XRP(100)), - cancel_time(env.now() + 1s), + env(escrow::create("alice", "bob", XRP(100)), + escrow::cancel_time(env.now() + 1s), fee(baseFee * 150)); env.close(); - env(finish("carol", "alice", seq1), fee(baseFee * 150)); + env(escrow::finish("carol", "alice", seq1), fee(baseFee * 150)); BEAST_EXPECT(env.balance("bob") == XRP(5100)); env.close(); @@ -340,14 +314,14 @@ struct Escrow_test : public beast::unit_test::suite // Creating an escrow without a finish time and a condition is // also allowed without fix1571: auto const seq2 = env.seq("alice"); - env(escrow("alice", "bob", XRP(100)), - cancel_time(env.now() + 1s), - condition(cb1), + env(escrow::create("alice", "bob", XRP(100)), + escrow::cancel_time(env.now() + 1s), + escrow::condition(escrow::cb1), fee(baseFee * 150)); env.close(); - env(finish("carol", "alice", seq2), - condition(cb1), - fulfillment(fb1), + env(escrow::finish("carol", "alice", seq2), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), fee(baseFee * 150)); BEAST_EXPECT(env.balance("bob") == XRP(5200)); } @@ -355,117 +329,131 @@ struct Escrow_test : public beast::unit_test::suite { testcase("Implied Finish Time (with fix1571)"); - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob", "carol"); env.close(); // Creating an escrow with only a cancel time is not allowed: - env(escrow("alice", "bob", XRP(100)), - cancel_time(env.now() + 90s), + env(escrow::create("alice", "bob", XRP(100)), + escrow::cancel_time(env.now() + 90s), fee(baseFee * 150), ter(temMALFORMED)); // Creating an escrow with only a cancel time and a condition is // allowed: auto const seq = env.seq("alice"); - env(escrow("alice", "bob", XRP(100)), - cancel_time(env.now() + 90s), - condition(cb1), + env(escrow::create("alice", "bob", XRP(100)), + escrow::cancel_time(env.now() + 90s), + escrow::condition(escrow::cb1), fee(baseFee * 150)); env.close(); - env(finish("carol", "alice", seq), - condition(cb1), - fulfillment(fb1), + env(escrow::finish("carol", "alice", seq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), fee(baseFee * 150)); BEAST_EXPECT(env.balance("bob") == XRP(5100)); } } void - testFails() + testFails(FeatureBitset features) { testcase("Failure Cases"); using namespace jtx; using namespace std::chrono; - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; - env.fund(XRP(5000), "alice", "bob"); + env.fund(XRP(5000), "alice", "bob", "gw"); env.close(); + // temINVALID_FLAG + env(escrow::create("alice", "bob", XRP(1000)), + escrow::finish_time(env.now() + 5s), + txflags(tfPassive), + ter(temINVALID_FLAG)); + // Finish time is in the past - env(escrow("alice", "bob", XRP(1000)), - finish_time(env.now() - 5s), + env(escrow::create("alice", "bob", XRP(1000)), + escrow::finish_time(env.now() - 5s), ter(tecNO_PERMISSION)); // Cancel time is in the past - env(escrow("alice", "bob", XRP(1000)), - condition(cb1), - cancel_time(env.now() - 5s), + env(escrow::create("alice", "bob", XRP(1000)), + escrow::condition(escrow::cb1), + escrow::cancel_time(env.now() - 5s), ter(tecNO_PERMISSION)); // no destination account - env(escrow("alice", "carol", XRP(1000)), - finish_time(env.now() + 1s), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::finish_time(env.now() + 1s), ter(tecNO_DST)); env.fund(XRP(5000), "carol"); // Using non-XRP: - env(escrow("alice", "carol", Account("alice")["USD"](500)), - finish_time(env.now() + 1s), - ter(temBAD_AMOUNT)); + bool const withTokenEscrow = + env.current()->rules().enabled(featureTokenEscrow); + { + // tecNO_PERMISSION: token escrow is enabled but the issuer did not + // set the asfAllowTrustLineLocking flag + auto const txResult = + withTokenEscrow ? ter(tecNO_PERMISSION) : ter(temBAD_AMOUNT); + env(escrow::create("alice", "carol", Account("alice")["USD"](500)), + escrow::finish_time(env.now() + 5s), + txResult); + } // Sending zero or no XRP: - env(escrow("alice", "carol", XRP(0)), - finish_time(env.now() + 1s), + env(escrow::create("alice", "carol", XRP(0)), + escrow::finish_time(env.now() + 1s), ter(temBAD_AMOUNT)); - env(escrow("alice", "carol", XRP(-1000)), - finish_time(env.now() + 1s), + env(escrow::create("alice", "carol", XRP(-1000)), + escrow::finish_time(env.now() + 1s), ter(temBAD_AMOUNT)); // Fail if neither CancelAfter nor FinishAfter are specified: - env(escrow("alice", "carol", XRP(1)), ter(temBAD_EXPIRATION)); + env(escrow::create("alice", "carol", XRP(1)), ter(temBAD_EXPIRATION)); // Fail if neither a FinishTime nor a condition are attached: - env(escrow("alice", "carol", XRP(1)), - cancel_time(env.now() + 1s), + env(escrow::create("alice", "carol", XRP(1)), + escrow::cancel_time(env.now() + 1s), ter(temMALFORMED)); // Fail if FinishAfter has already passed: - env(escrow("alice", "carol", XRP(1)), - finish_time(env.now() - 1s), + env(escrow::create("alice", "carol", XRP(1)), + escrow::finish_time(env.now() - 1s), ter(tecNO_PERMISSION)); // If both CancelAfter and FinishAfter are set, then CancelAfter must // be strictly later than FinishAfter. - env(escrow("alice", "carol", XRP(1)), - condition(cb1), - finish_time(env.now() + 10s), - cancel_time(env.now() + 10s), + env(escrow::create("alice", "carol", XRP(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 10s), + escrow::cancel_time(env.now() + 10s), ter(temBAD_EXPIRATION)); - env(escrow("alice", "carol", XRP(1)), - condition(cb1), - finish_time(env.now() + 10s), - cancel_time(env.now() + 5s), + env(escrow::create("alice", "carol", XRP(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 10s), + escrow::cancel_time(env.now() + 5s), ter(temBAD_EXPIRATION)); // Carol now requires the use of a destination tag env(fset("carol", asfRequireDest)); // missing destination tag - env(escrow("alice", "carol", XRP(1)), - condition(cb1), - cancel_time(env.now() + 1s), + env(escrow::create("alice", "carol", XRP(1)), + escrow::condition(escrow::cb1), + escrow::cancel_time(env.now() + 1s), ter(tecDST_TAG_NEEDED)); // Success! - env(escrow("alice", "carol", XRP(1)), - condition(cb1), - cancel_time(env.now() + 1s), + env(escrow::create("alice", "carol", XRP(1)), + escrow::condition(escrow::cb1), + escrow::cancel_time(env.now() + 1s), dtag(1)); { // Fail if the sender wants to send more than he has: @@ -474,29 +462,29 @@ struct Escrow_test : public beast::unit_test::suite drops(env.current()->fees().increment); env.fund(accountReserve + accountIncrement + XRP(50), "daniel"); - env(escrow("daniel", "bob", XRP(51)), - finish_time(env.now() + 1s), + env(escrow::create("daniel", "bob", XRP(51)), + escrow::finish_time(env.now() + 1s), ter(tecUNFUNDED)); env.fund(accountReserve + accountIncrement + XRP(50), "evan"); - env(escrow("evan", "bob", XRP(50)), - finish_time(env.now() + 1s), + env(escrow::create("evan", "bob", XRP(50)), + escrow::finish_time(env.now() + 1s), ter(tecUNFUNDED)); env.fund(accountReserve, "frank"); - env(escrow("frank", "bob", XRP(1)), - finish_time(env.now() + 1s), + env(escrow::create("frank", "bob", XRP(1)), + escrow::finish_time(env.now() + 1s), ter(tecINSUFFICIENT_RESERVE)); } { // Specify incorrect sequence number env.fund(XRP(5000), "hannah"); auto const seq = env.seq("hannah"); - env(escrow("hannah", "hannah", XRP(10)), - finish_time(env.now() + 1s), + env(escrow::create("hannah", "hannah", XRP(10)), + escrow::finish_time(env.now() + 1s), fee(150 * baseFee)); env.close(); - env(finish("hannah", "hannah", seq + 7), + env(escrow::finish("hannah", "hannah", seq + 7), fee(150 * baseFee), ter(tecNO_TARGET)); } @@ -505,18 +493,19 @@ struct Escrow_test : public beast::unit_test::suite env.fund(XRP(5000), "ivan"); auto const seq = env.seq("ivan"); - env(escrow("ivan", "ivan", XRP(10)), finish_time(env.now() + 1s)); + env(escrow::create("ivan", "ivan", XRP(10)), + escrow::finish_time(env.now() + 1s)); env.close(); - env(finish("ivan", "ivan", seq), - condition(cb1), - fulfillment(fb1), + env(escrow::finish("ivan", "ivan", seq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); } } void - testLockup() + testLockup(FeatureBitset features) { testcase("Lockup"); @@ -525,49 +514,50 @@ struct Escrow_test : public beast::unit_test::suite { // Unconditional - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob"); auto const seq = env.seq("alice"); - env(escrow("alice", "alice", XRP(1000)), - finish_time(env.now() + 5s)); + env(escrow::create("alice", "alice", XRP(1000)), + escrow::finish_time(env.now() + 5s)); env.require(balance("alice", XRP(4000) - drops(baseFee))); // Not enough time has elapsed for a finish and canceling isn't // possible. - env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("bob", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::finish("bob", "alice", seq), ter(tecNO_PERMISSION)); env.close(); // Cancel continues to not be possible - env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); // Finish should succeed. Verify funds. - env(finish("bob", "alice", seq)); + env(escrow::finish("bob", "alice", seq)); env.require(balance("alice", XRP(5000) - drops(baseFee))); } { // Unconditionally pay from Alice to Bob. Zelda (neither source nor // destination) signs all cancels and finishes. This shows that // Escrow will make a payment to Bob with no intervention from Bob. - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob", "zelda"); auto const seq = env.seq("alice"); - env(escrow("alice", "bob", XRP(1000)), finish_time(env.now() + 5s)); + env(escrow::create("alice", "bob", XRP(1000)), + escrow::finish_time(env.now() + 5s)); env.require(balance("alice", XRP(4000) - drops(baseFee))); // Not enough time has elapsed for a finish and canceling isn't // possible. - env(cancel("zelda", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("zelda", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::cancel("zelda", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::finish("zelda", "alice", seq), ter(tecNO_PERMISSION)); env.close(); // Cancel continues to not be possible - env(cancel("zelda", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::cancel("zelda", "alice", seq), ter(tecNO_PERMISSION)); // Finish should succeed. Verify funds. - env(finish("zelda", "alice", seq)); + env(escrow::finish("zelda", "alice", seq)); env.close(); env.require(balance("alice", XRP(4000) - drops(baseFee))); @@ -576,7 +566,7 @@ struct Escrow_test : public beast::unit_test::suite } { // Bob sets DepositAuth so only Bob can finish the escrow. - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob", "zelda"); @@ -584,27 +574,28 @@ struct Escrow_test : public beast::unit_test::suite env.close(); auto const seq = env.seq("alice"); - env(escrow("alice", "bob", XRP(1000)), finish_time(env.now() + 5s)); + env(escrow::create("alice", "bob", XRP(1000)), + escrow::finish_time(env.now() + 5s)); env.require(balance("alice", XRP(4000) - drops(baseFee))); // Not enough time has elapsed for a finish and canceling isn't // possible. - env(cancel("zelda", "alice", seq), ter(tecNO_PERMISSION)); - env(cancel("alice", "alice", seq), ter(tecNO_PERMISSION)); - env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("zelda", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("alice", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("bob", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::cancel("zelda", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::cancel("alice", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::finish("zelda", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::finish("alice", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::finish("bob", "alice", seq), ter(tecNO_PERMISSION)); env.close(); // Cancel continues to not be possible. Finish will only succeed for // Bob, because of DepositAuth. - env(cancel("zelda", "alice", seq), ter(tecNO_PERMISSION)); - env(cancel("alice", "alice", seq), ter(tecNO_PERMISSION)); - env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("zelda", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("alice", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("bob", "alice", seq)); + env(escrow::cancel("zelda", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::cancel("alice", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::finish("zelda", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::finish("alice", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::finish("bob", "alice", seq)); env.close(); env.require(balance("alice", XRP(4000) - (baseFee * 5))); @@ -614,7 +605,7 @@ struct Escrow_test : public beast::unit_test::suite { // Bob sets DepositAuth but preauthorizes Zelda, so Zelda can // finish the escrow. - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob", "zelda"); @@ -624,15 +615,16 @@ struct Escrow_test : public beast::unit_test::suite env.close(); auto const seq = env.seq("alice"); - env(escrow("alice", "bob", XRP(1000)), finish_time(env.now() + 5s)); + env(escrow::create("alice", "bob", XRP(1000)), + escrow::finish_time(env.now() + 5s)); env.require(balance("alice", XRP(4000) - drops(baseFee))); env.close(); // DepositPreauth allows Finish to succeed for either Zelda or // Bob. But Finish won't succeed for Alice since she is not // preauthorized. - env(finish("alice", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("zelda", "alice", seq)); + env(escrow::finish("alice", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::finish("zelda", "alice", seq)); env.close(); env.require(balance("alice", XRP(4000) - (baseFee * 2))); @@ -641,93 +633,97 @@ struct Escrow_test : public beast::unit_test::suite } { // Conditional - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob"); auto const seq = env.seq("alice"); - env(escrow("alice", "alice", XRP(1000)), - condition(cb2), - finish_time(env.now() + 5s)); + env(escrow::create("alice", "alice", XRP(1000)), + escrow::condition(escrow::cb2), + escrow::finish_time(env.now() + 5s)); env.require(balance("alice", XRP(4000) - drops(baseFee))); // Not enough time has elapsed for a finish and canceling isn't // possible. - env(cancel("alice", "alice", seq), ter(tecNO_PERMISSION)); - env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("alice", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("alice", "alice", seq), - condition(cb2), - fulfillment(fb2), + env(escrow::cancel("alice", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::finish("alice", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::finish("alice", "alice", seq), + escrow::condition(escrow::cb2), + escrow::fulfillment(escrow::fb2), fee(150 * baseFee), ter(tecNO_PERMISSION)); - env(finish("bob", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("bob", "alice", seq), - condition(cb2), - fulfillment(fb2), + env(escrow::finish("bob", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb2), + escrow::fulfillment(escrow::fb2), fee(150 * baseFee), ter(tecNO_PERMISSION)); env.close(); // Cancel continues to not be possible. Finish is possible but // requires the fulfillment associated with the escrow. - env(cancel("alice", "alice", seq), ter(tecNO_PERMISSION)); - env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("bob", "alice", seq), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("alice", "alice", seq), ter(tecCRYPTOCONDITION_ERROR)); + env(escrow::cancel("alice", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::finish("bob", "alice", seq), + ter(tecCRYPTOCONDITION_ERROR)); + env(escrow::finish("alice", "alice", seq), + ter(tecCRYPTOCONDITION_ERROR)); env.close(); - env(finish("bob", "alice", seq), - condition(cb2), - fulfillment(fb2), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb2), + escrow::fulfillment(escrow::fb2), fee(150 * baseFee)); } { // Self-escrowed conditional with DepositAuth. - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob"); auto const seq = env.seq("alice"); - env(escrow("alice", "alice", XRP(1000)), - condition(cb3), - finish_time(env.now() + 5s)); + env(escrow::create("alice", "alice", XRP(1000)), + escrow::condition(escrow::cb3), + escrow::finish_time(env.now() + 5s)); env.require(balance("alice", XRP(4000) - drops(baseFee))); env.close(); // Finish is now possible but requires the cryptocondition. - env(finish("bob", "alice", seq), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("alice", "alice", seq), ter(tecCRYPTOCONDITION_ERROR)); + env(escrow::finish("bob", "alice", seq), + ter(tecCRYPTOCONDITION_ERROR)); + env(escrow::finish("alice", "alice", seq), + ter(tecCRYPTOCONDITION_ERROR)); // Enable deposit authorization. After this only Alice can finish // the escrow. env(fset("alice", asfDepositAuth)); env.close(); - env(finish("alice", "alice", seq), - condition(cb2), - fulfillment(fb2), + env(escrow::finish("alice", "alice", seq), + escrow::condition(escrow::cb2), + escrow::fulfillment(escrow::fb2), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(cb3), - fulfillment(fb3), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb3), + escrow::fulfillment(escrow::fb3), fee(150 * baseFee), ter(tecNO_PERMISSION)); - env(finish("alice", "alice", seq), - condition(cb3), - fulfillment(fb3), + env(escrow::finish("alice", "alice", seq), + escrow::condition(escrow::cb3), + escrow::fulfillment(escrow::fb3), fee(150 * baseFee)); } { // Self-escrowed conditional with DepositAuth and DepositPreauth. - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob", "zelda"); auto const seq = env.seq("alice"); - env(escrow("alice", "alice", XRP(1000)), - condition(cb3), - finish_time(env.now() + 5s)); + env(escrow::create("alice", "alice", XRP(1000)), + escrow::condition(escrow::cb3), + escrow::finish_time(env.now() + 5s)); env.require(balance("alice", XRP(4000) - drops(baseFee))); env.close(); @@ -737,34 +733,37 @@ struct Escrow_test : public beast::unit_test::suite env.close(); // Finish is now possible but requires the cryptocondition. - env(finish("alice", "alice", seq), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("zelda", "alice", seq), ter(tecCRYPTOCONDITION_ERROR)); + env(escrow::finish("alice", "alice", seq), + ter(tecCRYPTOCONDITION_ERROR)); + env(escrow::finish("bob", "alice", seq), + ter(tecCRYPTOCONDITION_ERROR)); + env(escrow::finish("zelda", "alice", seq), + ter(tecCRYPTOCONDITION_ERROR)); // Alice enables deposit authorization. After this only Alice or // Zelda (because Zelda is preauthorized) can finish the escrow. env(fset("alice", asfDepositAuth)); env.close(); - env(finish("alice", "alice", seq), - condition(cb2), - fulfillment(fb2), + env(escrow::finish("alice", "alice", seq), + escrow::condition(escrow::cb2), + escrow::fulfillment(escrow::fb2), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(cb3), - fulfillment(fb3), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb3), + escrow::fulfillment(escrow::fb3), fee(150 * baseFee), ter(tecNO_PERMISSION)); - env(finish("zelda", "alice", seq), - condition(cb3), - fulfillment(fb3), + env(escrow::finish("zelda", "alice", seq), + escrow::condition(escrow::cb3), + escrow::fulfillment(escrow::fb3), fee(150 * baseFee)); } } void - testEscrowConditions() + testEscrowConditions(FeatureBitset features) { testcase("Escrow with CryptoConditions"); @@ -772,126 +771,127 @@ struct Escrow_test : public beast::unit_test::suite using namespace std::chrono; { // Test cryptoconditions - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob", "carol"); auto const seq = env.seq("alice"); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 0); - env(escrow("alice", "carol", XRP(1000)), - condition(cb1), - cancel_time(env.now() + 1s)); + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(escrow::cb1), + escrow::cancel_time(env.now() + 1s)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); env.require(balance("alice", XRP(4000) - drops(baseFee))); env.require(balance("carol", XRP(5000))); - env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); // Attempt to finish without a fulfillment - env(finish("bob", "alice", seq), ter(tecCRYPTOCONDITION_ERROR)); + env(escrow::finish("bob", "alice", seq), + ter(tecCRYPTOCONDITION_ERROR)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); // Attempt to finish with a condition instead of a fulfillment - env(finish("bob", "alice", seq), - condition(cb1), - fulfillment(cb1), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::cb1), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); - env(finish("bob", "alice", seq), - condition(cb1), - fulfillment(cb2), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::cb2), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); - env(finish("bob", "alice", seq), - condition(cb1), - fulfillment(cb3), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::cb3), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); // Attempt to finish with an incorrect condition and various // combinations of correct and incorrect fulfillments. - env(finish("bob", "alice", seq), - condition(cb2), - fulfillment(fb1), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb2), + escrow::fulfillment(escrow::fb1), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); - env(finish("bob", "alice", seq), - condition(cb2), - fulfillment(fb2), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb2), + escrow::fulfillment(escrow::fb2), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); - env(finish("bob", "alice", seq), - condition(cb2), - fulfillment(fb3), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb2), + escrow::fulfillment(escrow::fb3), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); // Attempt to finish with the correct condition & fulfillment - env(finish("bob", "alice", seq), - condition(cb1), - fulfillment(fb1), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), fee(150 * baseFee)); // SLE removed on finish BEAST_EXPECT(!env.le(keylet::escrow(Account("alice").id(), seq))); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 0); env.require(balance("carol", XRP(6000))); - env(cancel("bob", "alice", seq), ter(tecNO_TARGET)); + env(escrow::cancel("bob", "alice", seq), ter(tecNO_TARGET)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 0); - env(cancel("bob", "carol", 1), ter(tecNO_TARGET)); + env(escrow::cancel("bob", "carol", 1), ter(tecNO_TARGET)); } { // Test cancel when condition is present - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob", "carol"); auto const seq = env.seq("alice"); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 0); - env(escrow("alice", "carol", XRP(1000)), - condition(cb2), - cancel_time(env.now() + 1s)); + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(escrow::cb2), + escrow::cancel_time(env.now() + 1s)); env.close(); env.require(balance("alice", XRP(4000) - drops(baseFee))); // balance restored on cancel - env(cancel("bob", "alice", seq)); + env(escrow::cancel("bob", "alice", seq)); env.require(balance("alice", XRP(5000) - drops(baseFee))); // SLE removed on cancel BEAST_EXPECT(!env.le(keylet::escrow(Account("alice").id(), seq))); } { - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob", "carol"); env.close(); auto const seq = env.seq("alice"); - env(escrow("alice", "carol", XRP(1000)), - condition(cb3), - cancel_time(env.now() + 1s)); + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(escrow::cb3), + escrow::cancel_time(env.now() + 1s)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); // cancel fails before expiration - env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); env.close(); // finish fails after expiration - env(finish("bob", "alice", seq), - condition(cb3), - fulfillment(fb3), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb3), + escrow::fulfillment(escrow::fb3), fee(150 * baseFee), ter(tecNO_PERMISSION)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); env.require(balance("carol", XRP(5000))); } { // Test long & short conditions during creation - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), "alice", "bob", "carol"); std::vector v; - v.resize(cb1.size() + 2, 0x78); - std::memcpy(v.data() + 1, cb1.data(), cb1.size()); + v.resize(escrow::cb1.size() + 2, 0x78); + std::memcpy(v.data() + 1, escrow::cb1.data(), escrow::cb1.size()); auto const p = v.data(); auto const s = v.size(); @@ -900,63 +900,63 @@ struct Escrow_test : public beast::unit_test::suite // All these are expected to fail, because the // condition we pass in is malformed in some way - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{p, s}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{p, s}), + escrow::cancel_time(ts), ter(temMALFORMED)); - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{p, s - 1}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{p, s - 1}), + escrow::cancel_time(ts), ter(temMALFORMED)); - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{p, s - 2}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{p, s - 2}), + escrow::cancel_time(ts), ter(temMALFORMED)); - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{p + 1, s - 1}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{p + 1, s - 1}), + escrow::cancel_time(ts), ter(temMALFORMED)); - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{p + 1, s - 3}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{p + 1, s - 3}), + escrow::cancel_time(ts), ter(temMALFORMED)); - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{p + 2, s - 2}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{p + 2, s - 2}), + escrow::cancel_time(ts), ter(temMALFORMED)); - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{p + 2, s - 3}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{p + 2, s - 3}), + escrow::cancel_time(ts), ter(temMALFORMED)); auto const seq = env.seq("alice"); auto const baseFee = env.current()->fees().base; - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{p + 1, s - 2}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{p + 1, s - 2}), + escrow::cancel_time(ts), fee(10 * baseFee)); - env(finish("bob", "alice", seq), - condition(cb1), - fulfillment(fb1), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), fee(150 * baseFee)); env.require(balance("alice", XRP(4000) - drops(10 * baseFee))); env.require(balance("bob", XRP(5000) - drops(150 * baseFee))); env.require(balance("carol", XRP(6000))); } { // Test long and short conditions & fulfillments during finish - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), "alice", "bob", "carol"); std::vector cv; - cv.resize(cb2.size() + 2, 0x78); - std::memcpy(cv.data() + 1, cb2.data(), cb2.size()); + cv.resize(escrow::cb2.size() + 2, 0x78); + std::memcpy(cv.data() + 1, escrow::cb2.data(), escrow::cb2.size()); auto const cp = cv.data(); auto const cs = cv.size(); std::vector fv; - fv.resize(fb2.size() + 2, 0x13); - std::memcpy(fv.data() + 1, fb2.data(), fb2.size()); + fv.resize(escrow::fb2.size() + 2, 0x13); + std::memcpy(fv.data() + 1, escrow::fb2.data(), escrow::fb2.size()); auto const fp = fv.data(); auto const fs = fv.size(); @@ -965,180 +965,182 @@ struct Escrow_test : public beast::unit_test::suite // All these are expected to fail, because the // condition we pass in is malformed in some way - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{cp, cs}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{cp, cs}), + escrow::cancel_time(ts), ter(temMALFORMED)); - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{cp, cs - 1}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{cp, cs - 1}), + escrow::cancel_time(ts), ter(temMALFORMED)); - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{cp, cs - 2}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{cp, cs - 2}), + escrow::cancel_time(ts), ter(temMALFORMED)); - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{cp + 1, cs - 1}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{cp + 1, cs - 1}), + escrow::cancel_time(ts), ter(temMALFORMED)); - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{cp + 1, cs - 3}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{cp + 1, cs - 3}), + escrow::cancel_time(ts), ter(temMALFORMED)); - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{cp + 2, cs - 2}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{cp + 2, cs - 2}), + escrow::cancel_time(ts), ter(temMALFORMED)); - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{cp + 2, cs - 3}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{cp + 2, cs - 3}), + escrow::cancel_time(ts), ter(temMALFORMED)); auto const seq = env.seq("alice"); auto const baseFee = env.current()->fees().base; - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{cp + 1, cs - 2}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{cp + 1, cs - 2}), + escrow::cancel_time(ts), fee(10 * baseFee)); // Now, try to fulfill using the same sequence of // malformed conditions. - env(finish("bob", "alice", seq), - condition(Slice{cp, cs}), - fulfillment(Slice{fp, fs}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp, cs}), + escrow::fulfillment(Slice{fp, fs}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{cp, cs - 1}), - fulfillment(Slice{fp, fs}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp, cs - 1}), + escrow::fulfillment(Slice{fp, fs}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{cp, cs - 2}), - fulfillment(Slice{fp, fs}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp, cs - 2}), + escrow::fulfillment(Slice{fp, fs}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{cp + 1, cs - 1}), - fulfillment(Slice{fp, fs}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp + 1, cs - 1}), + escrow::fulfillment(Slice{fp, fs}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{cp + 1, cs - 3}), - fulfillment(Slice{fp, fs}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp + 1, cs - 3}), + escrow::fulfillment(Slice{fp, fs}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{cp + 2, cs - 2}), - fulfillment(Slice{fp, fs}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp + 2, cs - 2}), + escrow::fulfillment(Slice{fp, fs}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{cp + 2, cs - 3}), - fulfillment(Slice{fp, fs}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp + 2, cs - 3}), + escrow::fulfillment(Slice{fp, fs}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); // Now, using the correct condition, try malformed fulfillments: - env(finish("bob", "alice", seq), - condition(Slice{cp + 1, cs - 2}), - fulfillment(Slice{fp, fs}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp + 1, cs - 2}), + escrow::fulfillment(Slice{fp, fs}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{cp + 1, cs - 2}), - fulfillment(Slice{fp, fs - 1}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp + 1, cs - 2}), + escrow::fulfillment(Slice{fp, fs - 1}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{cp + 1, cs - 2}), - fulfillment(Slice{fp, fs - 2}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp + 1, cs - 2}), + escrow::fulfillment(Slice{fp, fs - 2}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{cp + 1, cs - 2}), - fulfillment(Slice{fp + 1, fs - 1}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp + 1, cs - 2}), + escrow::fulfillment(Slice{fp + 1, fs - 1}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{cp + 1, cs - 2}), - fulfillment(Slice{fp + 1, fs - 3}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp + 1, cs - 2}), + escrow::fulfillment(Slice{fp + 1, fs - 3}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{cp + 1, cs - 2}), - fulfillment(Slice{fp + 1, fs - 3}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp + 1, cs - 2}), + escrow::fulfillment(Slice{fp + 1, fs - 3}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{cp + 1, cs - 2}), - fulfillment(Slice{fp + 2, fs - 2}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp + 1, cs - 2}), + escrow::fulfillment(Slice{fp + 2, fs - 2}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{cp + 1, cs - 2}), - fulfillment(Slice{fp + 2, fs - 3}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp + 1, cs - 2}), + escrow::fulfillment(Slice{fp + 2, fs - 3}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); // Now try for the right one - env(finish("bob", "alice", seq), - condition(cb2), - fulfillment(fb2), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb2), + escrow::fulfillment(escrow::fb2), fee(150 * baseFee)); env.require(balance("alice", XRP(4000) - drops(10 * baseFee))); env.require(balance("carol", XRP(6000))); } { // Test empty condition during creation and // empty condition & fulfillment during finish - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), "alice", "bob", "carol"); - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{}), - cancel_time(env.now() + 1s), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{}), + escrow::cancel_time(env.now() + 1s), ter(temMALFORMED)); auto const seq = env.seq("alice"); auto const baseFee = env.current()->fees().base; - env(escrow("alice", "carol", XRP(1000)), - condition(cb3), - cancel_time(env.now() + 1s)); + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(escrow::cb3), + escrow::cancel_time(env.now() + 1s)); - env(finish("bob", "alice", seq), - condition(Slice{}), - fulfillment(Slice{}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{}), + escrow::fulfillment(Slice{}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(cb3), - fulfillment(Slice{}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb3), + escrow::fulfillment(Slice{}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{}), - fulfillment(fb3), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{}), + escrow::fulfillment(escrow::fb3), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); // Assemble finish that is missing the Condition or the Fulfillment // since either both must be present, or neither can: - env(finish("bob", "alice", seq), condition(cb3), ter(temMALFORMED)); - env(finish("bob", "alice", seq), - fulfillment(fb3), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb3), + ter(temMALFORMED)); + env(escrow::finish("bob", "alice", seq), + escrow::fulfillment(escrow::fb3), ter(temMALFORMED)); // Now finish it. - env(finish("bob", "alice", seq), - condition(cb3), - fulfillment(fb3), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb3), + escrow::fulfillment(escrow::fb3), fee(150 * baseFee)); env.require(balance("carol", XRP(6000))); env.require(balance("alice", XRP(4000) - drops(baseFee))); } { // Test a condition other than PreimageSha256, which // would require a separate amendment - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), "alice", "bob"); std::array cb = { @@ -1150,15 +1152,15 @@ struct Escrow_test : public beast::unit_test::suite // FIXME: this transaction should, eventually, return temDISABLED // instead of temMALFORMED. - env(escrow("alice", "bob", XRP(1000)), - condition(cb), - cancel_time(env.now() + 1s), + env(escrow::create("alice", "bob", XRP(1000)), + escrow::condition(cb), + escrow::cancel_time(env.now() + 1s), ter(temMALFORMED)); } } void - testMetaAndOwnership() + testMetaAndOwnership(FeatureBitset features) { using namespace jtx; using namespace std::chrono; @@ -1170,14 +1172,14 @@ struct Escrow_test : public beast::unit_test::suite { testcase("Metadata to self"); - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), alice, bruce, carol); auto const aseq = env.seq(alice); auto const bseq = env.seq(bruce); - env(escrow(alice, alice, XRP(1000)), - finish_time(env.now() + 1s), - cancel_time(env.now() + 500s)); + env(escrow::create(alice, alice, XRP(1000)), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 500s)); BEAST_EXPECT( (*env.meta())[sfTransactionResult] == static_cast(tesSUCCESS)); @@ -1192,9 +1194,9 @@ struct Escrow_test : public beast::unit_test::suite std::find(aod.begin(), aod.end(), aa) != aod.end()); } - env(escrow(bruce, bruce, XRP(1000)), - finish_time(env.now() + 1s), - cancel_time(env.now() + 2s)); + env(escrow::create(bruce, bruce, XRP(1000)), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s)); BEAST_EXPECT( (*env.meta())[sfTransactionResult] == static_cast(tesSUCCESS)); @@ -1210,7 +1212,7 @@ struct Escrow_test : public beast::unit_test::suite } env.close(5s); - env(finish(alice, alice, aseq)); + env(escrow::finish(alice, alice, aseq)); { BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); BEAST_EXPECT( @@ -1229,7 +1231,7 @@ struct Escrow_test : public beast::unit_test::suite } env.close(5s); - env(cancel(bruce, bruce, bseq)); + env(escrow::cancel(bruce, bruce, bseq)); { BEAST_EXPECT(!env.le(keylet::escrow(bruce.id(), bseq))); BEAST_EXPECT( @@ -1245,19 +1247,20 @@ struct Escrow_test : public beast::unit_test::suite { testcase("Metadata to other"); - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), alice, bruce, carol); auto const aseq = env.seq(alice); auto const bseq = env.seq(bruce); - env(escrow(alice, bruce, XRP(1000)), finish_time(env.now() + 1s)); + env(escrow::create(alice, bruce, XRP(1000)), + escrow::finish_time(env.now() + 1s)); BEAST_EXPECT( (*env.meta())[sfTransactionResult] == static_cast(tesSUCCESS)); env.close(5s); - env(escrow(bruce, carol, XRP(1000)), - finish_time(env.now() + 1s), - cancel_time(env.now() + 2s)); + env(escrow::create(bruce, carol, XRP(1000)), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s)); BEAST_EXPECT( (*env.meta())[sfTransactionResult] == static_cast(tesSUCCESS)); @@ -1289,7 +1292,7 @@ struct Escrow_test : public beast::unit_test::suite } env.close(5s); - env(finish(alice, alice, aseq)); + env(escrow::finish(alice, alice, aseq)); { BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); BEAST_EXPECT(env.le(keylet::escrow(bruce.id(), bseq))); @@ -1311,7 +1314,7 @@ struct Escrow_test : public beast::unit_test::suite } env.close(5s); - env(cancel(bruce, bruce, bseq)); + env(escrow::cancel(bruce, bruce, bseq)); { BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); BEAST_EXPECT(!env.le(keylet::escrow(bruce.id(), bseq))); @@ -1335,13 +1338,13 @@ struct Escrow_test : public beast::unit_test::suite } void - testConsequences() + testConsequences(FeatureBitset features) { testcase("Consequences"); using namespace jtx; using namespace std::chrono; - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.memoize("alice"); @@ -1350,8 +1353,8 @@ struct Escrow_test : public beast::unit_test::suite { auto const jtx = env.jt( - escrow("alice", "carol", XRP(1000)), - finish_time(env.now() + 1s), + escrow::create("alice", "carol", XRP(1000)), + escrow::finish_time(env.now() + 1s), seq(1), fee(baseFee)); auto const pf = preflight( @@ -1368,7 +1371,7 @@ struct Escrow_test : public beast::unit_test::suite { auto const jtx = - env.jt(cancel("bob", "alice", 3), seq(1), fee(baseFee)); + env.jt(escrow::cancel("bob", "alice", 3), seq(1), fee(baseFee)); auto const pf = preflight( env.app(), env.current()->rules(), @@ -1383,7 +1386,7 @@ struct Escrow_test : public beast::unit_test::suite { auto const jtx = - env.jt(finish("bob", "alice", 3), seq(1), fee(baseFee)); + env.jt(escrow::finish("bob", "alice", 3), seq(1), fee(baseFee)); auto const pf = preflight( env.app(), env.current()->rules(), @@ -1398,7 +1401,7 @@ struct Escrow_test : public beast::unit_test::suite } void - testEscrowWithTickets() + testEscrowWithTickets(FeatureBitset features) { testcase("Escrow with tickets"); @@ -1409,7 +1412,7 @@ struct Escrow_test : public beast::unit_test::suite { // Create escrow and finish using tickets. - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), alice, bob); env.close(); @@ -1437,8 +1440,8 @@ struct Escrow_test : public beast::unit_test::suite auto const ts = env.now() + 97s; std::uint32_t const escrowSeq = aliceTicket; - env(escrow(alice, bob, XRP(1000)), - finish_time(ts), + env(escrow::create(alice, bob, XRP(1000)), + escrow::finish_time(ts), ticket::use(aliceTicket)); BEAST_EXPECT(env.seq(alice) == aliceRootSeq); env.require(tickets(alice, 0)); @@ -1448,7 +1451,7 @@ struct Escrow_test : public beast::unit_test::suite // prematurely. Note that each tec consumes one of bob's tickets. for (; env.now() < ts; env.close()) { - env(finish(bob, alice, escrowSeq), + env(escrow::finish(bob, alice, escrowSeq), fee(150 * baseFee), ticket::use(--bobTicket), ter(tecNO_PERMISSION)); @@ -1456,13 +1459,13 @@ struct Escrow_test : public beast::unit_test::suite } // bob tries to re-use a ticket, which is rejected. - env(finish(bob, alice, escrowSeq), + env(escrow::finish(bob, alice, escrowSeq), fee(150 * baseFee), ticket::use(bobTicket), ter(tefNO_TICKET)); // bob uses one of his remaining tickets. Success! - env(finish(bob, alice, escrowSeq), + env(escrow::finish(bob, alice, escrowSeq), fee(150 * baseFee), ticket::use(--bobTicket)); env.close(); @@ -1470,7 +1473,7 @@ struct Escrow_test : public beast::unit_test::suite } { // Create escrow and cancel using tickets. - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), alice, bob); env.close(); @@ -1497,9 +1500,9 @@ struct Escrow_test : public beast::unit_test::suite auto const ts = env.now() + 117s; std::uint32_t const escrowSeq = aliceTicket; - env(escrow(alice, bob, XRP(1000)), - condition(cb1), - cancel_time(ts), + env(escrow::create(alice, bob, XRP(1000)), + escrow::condition(escrow::cb1), + escrow::cancel_time(ts), ticket::use(aliceTicket)); BEAST_EXPECT(env.seq(alice) == aliceRootSeq); env.require(tickets(alice, 0)); @@ -1509,7 +1512,7 @@ struct Escrow_test : public beast::unit_test::suite // prematurely. for (; env.now() < ts; env.close()) { - env(cancel(bob, alice, escrowSeq), + env(escrow::cancel(bob, alice, escrowSeq), fee(150 * baseFee), ticket::use(bobTicket++), ter(tecNO_PERMISSION)); @@ -1517,16 +1520,16 @@ struct Escrow_test : public beast::unit_test::suite } // Verify that a finish won't work anymore. - env(finish(bob, alice, escrowSeq), - condition(cb1), - fulfillment(fb1), + env(escrow::finish(bob, alice, escrowSeq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), fee(150 * baseFee), ticket::use(bobTicket++), ter(tecNO_PERMISSION)); BEAST_EXPECT(env.seq(bob) == bobRootSeq); // Verify that the cancel succeeds. - env(cancel(bob, alice, escrowSeq), + env(escrow::cancel(bob, alice, escrowSeq), fee(150 * baseFee), ticket::use(bobTicket++)); env.close(); @@ -1538,7 +1541,7 @@ struct Escrow_test : public beast::unit_test::suite } void - testCredentials() + testCredentials(FeatureBitset features) { testcase("Test with credentials"); @@ -1555,12 +1558,13 @@ struct Escrow_test : public beast::unit_test::suite { // Credentials amendment not enabled - Env env(*this, supported_amendments() - featureCredentials); + Env env(*this, features - featureCredentials); env.fund(XRP(5000), alice, bob); env.close(); auto const seq = env.seq(alice); - env(escrow(alice, bob, XRP(1000)), finish_time(env.now() + 1s)); + env(escrow::create(alice, bob, XRP(1000)), + escrow::finish_time(env.now() + 1s)); env.close(); env(fset(bob, asfDepositAuth)); @@ -1571,13 +1575,13 @@ struct Escrow_test : public beast::unit_test::suite std::string const credIdx = "48004829F915654A81B11C4AB8218D96FED67F209B58328A72314FB6EA288B" "E4"; - env(finish(bob, alice, seq), + env(escrow::finish(bob, alice, seq), credentials::ids({credIdx}), ter(temDISABLED)); } { - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), alice, bob, carol, dillon, zelda); env.close(); @@ -1589,7 +1593,8 @@ struct Escrow_test : public beast::unit_test::suite std::string const credIdx = jv[jss::result][jss::index].asString(); auto const seq = env.seq(alice); - env(escrow(alice, bob, XRP(1000)), finish_time(env.now() + 50s)); + env(escrow::create(alice, bob, XRP(1000)), + escrow::finish_time(env.now() + 50s)); env.close(); // Bob require preauthorization @@ -1597,7 +1602,7 @@ struct Escrow_test : public beast::unit_test::suite env.close(); // Fail, credentials not accepted - env(finish(carol, alice, seq), + env(escrow::finish(carol, alice, seq), credentials::ids({credIdx}), ter(tecBAD_CREDENTIALS)); @@ -1607,12 +1612,12 @@ struct Escrow_test : public beast::unit_test::suite env.close(); // Fail, credentials doesn’t belong to root account - env(finish(dillon, alice, seq), + env(escrow::finish(dillon, alice, seq), credentials::ids({credIdx}), ter(tecBAD_CREDENTIALS)); // Fail, no depositPreauth - env(finish(carol, alice, seq), + env(escrow::finish(carol, alice, seq), credentials::ids({credIdx}), ter(tecNO_PERMISSION)); @@ -1621,7 +1626,7 @@ struct Escrow_test : public beast::unit_test::suite // Success env.close(); - env(finish(carol, alice, seq), credentials::ids({credIdx})); + env(escrow::finish(carol, alice, seq), credentials::ids({credIdx})); env.close(); } @@ -1629,7 +1634,7 @@ struct Escrow_test : public beast::unit_test::suite testcase("Escrow with credentials without depositPreauth"); using namespace std::chrono; - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), alice, bob, carol, dillon, zelda); env.close(); @@ -1643,7 +1648,8 @@ struct Escrow_test : public beast::unit_test::suite std::string const credIdx = jv[jss::result][jss::index].asString(); auto const seq = env.seq(alice); - env(escrow(alice, bob, XRP(1000)), finish_time(env.now() + 50s)); + env(escrow::create(alice, bob, XRP(1000)), + escrow::finish_time(env.now() + 50s)); // time advance env.close(); env.close(); @@ -1653,7 +1659,7 @@ struct Escrow_test : public beast::unit_test::suite env.close(); // Succeed, Bob doesn't require preauthorization - env(finish(carol, alice, seq), credentials::ids({credIdx})); + env(escrow::finish(carol, alice, seq), credentials::ids({credIdx})); env.close(); { @@ -1669,7 +1675,8 @@ struct Escrow_test : public beast::unit_test::suite .asString(); auto const seq = env.seq(alice); - env(escrow(alice, bob, XRP(1000)), finish_time(env.now() + 1s)); + env(escrow::create(alice, bob, XRP(1000)), + escrow::finish_time(env.now() + 1s)); env.close(); // Bob require preauthorization @@ -1679,27 +1686,38 @@ struct Escrow_test : public beast::unit_test::suite env.close(); // Use any valid credentials if account == dst - env(finish(bob, alice, seq), credentials::ids({credIdxBob})); + env(escrow::finish(bob, alice, seq), + credentials::ids({credIdxBob})); env.close(); } } } + void + testWithFeats(FeatureBitset features) + { + testEnablement(features); + testTiming(features); + testTags(features); + testDisallowXRP(features); + test1571(features); + testFails(features); + testLockup(features); + testEscrowConditions(features); + testMetaAndOwnership(features); + testConsequences(features); + testEscrowWithTickets(features); + testCredentials(features); + } + +public: void run() override { - testEnablement(); - testTiming(); - testTags(); - testDisallowXRP(); - test1571(); - testFails(); - testLockup(); - testEscrowConditions(); - testMetaAndOwnership(); - testConsequences(); - testEscrowWithTickets(); - testCredentials(); + using namespace test::jtx; + FeatureBitset const all{testable_amendments()}; + testWithFeats(all); + testWithFeats(all - featureTokenEscrow); } }; diff --git a/src/test/app/FixNFTokenPageLinks_test.cpp b/src/test/app/FixNFTokenPageLinks_test.cpp index f87d70aacf..a54e889960 100644 --- a/src/test/app/FixNFTokenPageLinks_test.cpp +++ b/src/test/app/FixNFTokenPageLinks_test.cpp @@ -139,7 +139,7 @@ class FixNFTokenPageLinks_test : public beast::unit_test::suite { // Verify that the LedgerStateFix transaction is disabled // without the fixNFTokenPageLinks amendment. - Env env{*this, supported_amendments() - fixNFTokenPageLinks}; + Env env{*this, testable_amendments() - fixNFTokenPageLinks}; env.fund(XRP(1000), alice); auto const linkFixFee = drops(env.current()->fees().increment); @@ -148,7 +148,7 @@ class FixNFTokenPageLinks_test : public beast::unit_test::suite ter(temDISABLED)); } - Env env{*this, supported_amendments()}; + Env env{*this, testable_amendments()}; env.fund(XRP(1000), alice); std::uint32_t const ticketSeq = env.seq(alice); env(ticket::create(alice, 1)); @@ -206,7 +206,7 @@ class FixNFTokenPageLinks_test : public beast::unit_test::suite Account const alice("alice"); - Env env{*this, supported_amendments()}; + Env env{*this, testable_amendments()}; env.fund(XRP(1000), alice); // These cases all return the same TER code, but they exercise @@ -259,7 +259,7 @@ class FixNFTokenPageLinks_test : public beast::unit_test::suite Account const carol("carol"); Account const daria("daria"); - Env env{*this, supported_amendments() - fixNFTokenPageLinks}; + Env env{*this, testable_amendments() - fixNFTokenPageLinks}; env.fund(XRP(1000), alice, bob, carol, daria); //********************************************************************** diff --git a/src/test/app/Flow_test.cpp b/src/test/app/Flow_test.cpp index ae65432ac7..0f40d70b57 100644 --- a/src/test/app/Flow_test.cpp +++ b/src/test/app/Flow_test.cpp @@ -494,6 +494,7 @@ struct Flow_test : public beast::unit_test::suite OfferCrossing::no, std::nullopt, smax, + std::nullopt, flowJournal); }(); @@ -598,158 +599,18 @@ struct Flow_test : public beast::unit_test::suite Account const bob("bob"); Account const carol("carol"); - { - // Simple payment through a gateway with a - // transfer rate - Env env(*this, features); + // Offer where the owner is also the issuer, sender pays fee + Env env(*this, features); - env.fund(XRP(10000), alice, bob, carol, gw); - env.close(); - env(rate(gw, 1.25)); - env.trust(USD(1000), alice, bob, carol); - env(pay(gw, alice, USD(50))); - env.require(balance(alice, USD(50))); - env(pay(alice, bob, USD(40)), sendmax(USD(50))); - env.require(balance(bob, USD(40)), balance(alice, USD(0))); - } - { - // transfer rate is not charged when issuer is src or dst - Env env(*this, features); - - env.fund(XRP(10000), alice, bob, carol, gw); - env.close(); - env(rate(gw, 1.25)); - env.trust(USD(1000), alice, bob, carol); - env(pay(gw, alice, USD(50))); - env.require(balance(alice, USD(50))); - env(pay(alice, gw, USD(40)), sendmax(USD(40))); - env.require(balance(alice, USD(10))); - } - { - // transfer fee on an offer - Env env(*this, features); - - env.fund(XRP(10000), alice, bob, carol, gw); - env.close(); - env(rate(gw, 1.25)); - env.trust(USD(1000), alice, bob, carol); - env(pay(gw, bob, USD(65))); - - env(offer(bob, XRP(50), USD(50))); - - env(pay(alice, carol, USD(50)), path(~USD), sendmax(XRP(50))); - env.require( - balance(alice, xrpMinusFee(env, 10000 - 50)), - balance(bob, USD(2.5)), // owner pays transfer fee - balance(carol, USD(50))); - } - - { - // Transfer fee two consecutive offers - Env env(*this, features); - - env.fund(XRP(10000), alice, bob, carol, gw); - env.close(); - env(rate(gw, 1.25)); - env.trust(USD(1000), alice, bob, carol); - env.trust(EUR(1000), alice, bob, carol); - env(pay(gw, bob, USD(50))); - env(pay(gw, bob, EUR(50))); - - env(offer(bob, XRP(50), USD(50))); - env(offer(bob, USD(50), EUR(50))); - - env(pay(alice, carol, EUR(40)), path(~USD, ~EUR), sendmax(XRP(40))); - env.require( - balance(alice, xrpMinusFee(env, 10000 - 40)), - balance(bob, USD(40)), - balance(bob, EUR(0)), - balance(carol, EUR(40))); - } - - { - // First pass through a strand redeems, second pass issues, no - // offers limiting step is not an endpoint - Env env(*this, features); - auto const USDA = alice["USD"]; - auto const USDB = bob["USD"]; - - env.fund(XRP(10000), alice, bob, carol, gw); - env.close(); - env(rate(gw, 1.25)); - env.trust(USD(1000), alice, bob, carol); - env.trust(USDA(1000), bob); - env.trust(USDB(1000), gw); - env(pay(gw, bob, USD(50))); - // alice -> bob -> gw -> carol. $50 should have transfer fee; $10, - // no fee - env(pay(alice, carol, USD(50)), path(bob), sendmax(USDA(60))); - env.require( - balance(bob, USD(-10)), - balance(bob, USDA(60)), - balance(carol, USD(50))); - } - { - // First pass through a strand redeems, second pass issues, through - // an offer limiting step is not an endpoint - Env env(*this, features); - auto const USDA = alice["USD"]; - auto const USDB = bob["USD"]; - Account const dan("dan"); - - env.fund(XRP(10000), alice, bob, carol, dan, gw); - env.close(); - env(rate(gw, 1.25)); - env.trust(USD(1000), alice, bob, carol, dan); - env.trust(EUR(1000), carol, dan); - env.trust(USDA(1000), bob); - env.trust(USDB(1000), gw); - env(pay(gw, bob, USD(50))); - env(pay(gw, dan, EUR(100))); - env(offer(dan, USD(100), EUR(100))); - // alice -> bob -> gw -> carol. $50 should have transfer fee; $10, - // no fee - env(pay(alice, carol, EUR(50)), - path(bob, gw, ~EUR), - sendmax(USDA(60)), - txflags(tfNoRippleDirect)); - env.require( - balance(bob, USD(-10)), - balance(bob, USDA(60)), - balance(dan, USD(50)), - balance(dan, EUR(37.5)), - balance(carol, EUR(50))); - } - - { - // Offer where the owner is also the issuer, owner pays fee - Env env(*this, features); - - env.fund(XRP(10000), alice, bob, gw); - env.close(); - env(rate(gw, 1.25)); - env.trust(USD(1000), alice, bob); - env(offer(gw, XRP(100), USD(100))); - env(pay(alice, bob, USD(100)), sendmax(XRP(100))); - env.require( - balance(alice, xrpMinusFee(env, 10000 - 100)), - balance(bob, USD(100))); - } - if (!features[featureOwnerPaysFee]) - { - // Offer where the owner is also the issuer, sender pays fee - Env env(*this, features); - - env.fund(XRP(10000), alice, bob, gw); - env.close(); - env(rate(gw, 1.25)); - env.trust(USD(1000), alice, bob); - env(offer(gw, XRP(125), USD(125))); - env(pay(alice, bob, USD(100)), sendmax(XRP(200))); - env.require( - balance(alice, xrpMinusFee(env, 10000 - 125)), - balance(bob, USD(100))); - } + env.fund(XRP(10000), alice, bob, gw); + env.close(); + env(rate(gw, 1.25)); + env.trust(USD(1000), alice, bob); + env(offer(gw, XRP(125), USD(125))); + env(pay(alice, bob, USD(100)), sendmax(XRP(200))); + env.require( + balance(alice, xrpMinusFee(env, 10000 - 125)), + balance(bob, USD(100))); } void @@ -1333,8 +1194,8 @@ struct Flow_test : public beast::unit_test::suite { auto const feats = [&withFix]() -> FeatureBitset { if (withFix) - return supported_amendments(); - return supported_amendments() - FeatureBitset{fix1781}; + return testable_amendments(); + return testable_amendments() - FeatureBitset{fix1781}; }(); { // Payment path starting with XRP @@ -1444,7 +1305,6 @@ struct Flow_test : public beast::unit_test::suite testWithFeats(FeatureBitset features) { using namespace jtx; - FeatureBitset const ownerPaysFee{featureOwnerPaysFee}; FeatureBitset const reducedOffersV2(fixReducedOffersV2); testLineQuality(features); @@ -1452,9 +1312,7 @@ struct Flow_test : public beast::unit_test::suite testBookStep(features - reducedOffersV2); testDirectStep(features); testBookStep(features); - testDirectStep(features | ownerPaysFee); - testBookStep(features | ownerPaysFee); - testTransferRate(features | ownerPaysFee); + testTransferRate(features); testSelfPayment1(features); testSelfPayment2(features); testSelfFundedXRPEndpoint(false, features); @@ -1474,8 +1332,8 @@ struct Flow_test : public beast::unit_test::suite testRIPD1449(); using namespace jtx; - auto const sa = supported_amendments(); - testWithFeats(sa - featureFlowCross); + auto const sa = testable_amendments(); + testWithFeats(sa - featurePermissionedDEX); testWithFeats(sa); testEmptyStrand(sa); } @@ -1487,16 +1345,16 @@ struct Flow_manual_test : public Flow_test run() override { using namespace jtx; - auto const all = supported_amendments(); - FeatureBitset const flowCross{featureFlowCross}; + auto const all = testable_amendments(); FeatureBitset const f1513{fix1513}; + FeatureBitset const permDex{featurePermissionedDEX}; - testWithFeats(all - flowCross - f1513); - testWithFeats(all - flowCross); - testWithFeats(all - f1513); + testWithFeats(all - f1513 - permDex); + testWithFeats(all - permDex); testWithFeats(all); - testEmptyStrand(all - f1513); + testEmptyStrand(all - f1513 - permDex); + testEmptyStrand(all - permDex); testEmptyStrand(all); } }; diff --git a/src/test/app/Freeze_test.cpp b/src/test/app/Freeze_test.cpp index 36578cbc6b..3bde3a30af 100644 --- a/src/test/app/Freeze_test.cpp +++ b/src/test/app/Freeze_test.cpp @@ -961,24 +961,12 @@ class Freeze_test : public beast::unit_test::suite env.close(); // test: A1 wants to buy, must fail - if (features[featureFlowCross]) - { - env(offer(A1, USD(1), XRP(2)), - txflags(tfFillOrKill), - ter(tecKILLED)); - env.close(); - env.require( - balance(A1, USD(1002)), - balance(A2, USD(997)), - offers(A1, 0)); - } - else - { - // The transaction that should be here would succeed. - // I don't want to adjust balances in following tests. Flow - // cross feature flag is not relevant to this particular test - // case so we're not missing out some corner cases checks. - } + env(offer(A1, USD(1), XRP(2)), + txflags(tfFillOrKill), + ter(tecKILLED)); + env.close(); + env.require( + balance(A1, USD(1002)), balance(A2, USD(997)), offers(A1, 0)); // test: A1 can create passive sell offer env(offer(A1, XRP(2), USD(1)), txflags(tfPassive)); @@ -1885,6 +1873,31 @@ class Freeze_test : public beast::unit_test::suite env.close(); } + // Testing A1 nft buy offer when A2 deep frozen by issuer + if (features[featureDeepFreeze] && + features[fixEnforceNFTokenTrustlineV2]) + { + env(trust(G1, A2["USD"](0), tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + uint256 const nftID{token::getNextID(env, A2, 0u, tfTransferable)}; + env(token::mint(A2, 0), txflags(tfTransferable)); + env.close(); + + auto const buyIdx = keylet::nftoffer(A1, env.seq(A1)).key; + env(token::createOffer(A1, nftID, USD(10)), token::owner(A2)); + env.close(); + + env(token::acceptBuyOffer(A2, buyIdx), ter(tecFROZEN)); + env.close(); + + env(trust(G1, A2["USD"](0), tfClearFreeze | tfClearDeepFreeze)); + env.close(); + + env(token::acceptBuyOffer(A2, buyIdx)); + env.close(); + } + // Testing A2 nft offer sell when A2 frozen by currency holder { auto const sellOfferIndex = createNFTSellOffer(env, A2, USD(10)); @@ -1944,6 +1957,68 @@ class Freeze_test : public beast::unit_test::suite env(trust(A2, limit, tfClearFreeze | tfClearDeepFreeze)); env.close(); } + + // Testing brokered offer acceptance + if (features[featureDeepFreeze] && + features[fixEnforceNFTokenTrustlineV2]) + { + Account broker{"broker"}; + env.fund(XRP(10000), broker); + env.close(); + env(trust(G1, broker["USD"](1000), tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + uint256 const nftID{token::getNextID(env, A2, 0u, tfTransferable)}; + env(token::mint(A2, 0), txflags(tfTransferable)); + env.close(); + + uint256 const sellIdx = keylet::nftoffer(A2, env.seq(A2)).key; + env(token::createOffer(A2, nftID, USD(10)), txflags(tfSellNFToken)); + env.close(); + auto const buyIdx = keylet::nftoffer(A1, env.seq(A1)).key; + env(token::createOffer(A1, nftID, USD(11)), token::owner(A2)); + env.close(); + + env(token::brokerOffers(broker, buyIdx, sellIdx), + token::brokerFee(USD(1)), + ter(tecFROZEN)); + env.close(); + } + + // Testing transfer fee + if (features[featureDeepFreeze] && + features[fixEnforceNFTokenTrustlineV2]) + { + Account minter{"minter"}; + env.fund(XRP(10000), minter); + env.close(); + env(trust(G1, minter["USD"](1000))); + env.close(); + + uint256 const nftID{ + token::getNextID(env, minter, 0u, tfTransferable, 1u)}; + env(token::mint(minter, 0), + token::xferFee(1u), + txflags(tfTransferable)); + env.close(); + + uint256 const minterSellIdx = + keylet::nftoffer(minter, env.seq(minter)).key; + env(token::createOffer(minter, nftID, drops(1)), + txflags(tfSellNFToken)); + env.close(); + env(token::acceptSellOffer(A2, minterSellIdx)); + env.close(); + + uint256 const sellIdx = keylet::nftoffer(A2, env.seq(A2)).key; + env(token::createOffer(A2, nftID, USD(100)), + txflags(tfSellNFToken)); + env.close(); + env(trust(G1, minter["USD"](1000), tfSetFreeze | tfSetDeepFreeze)); + env.close(); + env(token::acceptSellOffer(A1, sellIdx), ter(tecFROZEN)); + env.close(); + } } // Helper function to extract trustline flags from open ledger @@ -2019,9 +2094,14 @@ public: testNFTOffersWhenFreeze(features); }; using namespace test::jtx; - auto const sa = supported_amendments(); - testAll(sa - featureFlowCross - featureDeepFreeze); - testAll(sa - featureFlowCross); + auto const sa = testable_amendments(); + testAll( + sa - featureDeepFreeze - featurePermissionedDEX - + fixEnforceNFTokenTrustlineV2); + testAll(sa - featurePermissionedDEX - fixEnforceNFTokenTrustlineV2); + testAll(sa - featureDeepFreeze - featurePermissionedDEX); + testAll(sa - featurePermissionedDEX); + testAll(sa - fixEnforceNFTokenTrustlineV2); testAll(sa - featureDeepFreeze); testAll(sa); } diff --git a/src/test/app/LPTokenTransfer_test.cpp b/src/test/app/LPTokenTransfer_test.cpp index 96e621dccf..e95e974547 100644 --- a/src/test/app/LPTokenTransfer_test.cpp +++ b/src/test/app/LPTokenTransfer_test.cpp @@ -467,7 +467,7 @@ public: void run() override { - FeatureBitset const all{jtx::supported_amendments()}; + FeatureBitset const all{jtx::testable_amendments()}; for (auto const features : {all, all - fixFrozenLPTokenTransfer}) { diff --git a/src/test/app/LedgerMaster_test.cpp b/src/test/app/LedgerMaster_test.cpp index 19664616b1..828e4b09c2 100644 --- a/src/test/app/LedgerMaster_test.cpp +++ b/src/test/app/LedgerMaster_test.cpp @@ -124,7 +124,7 @@ public: run() override { using namespace test::jtx; - FeatureBitset const all{supported_amendments()}; + FeatureBitset const all{testable_amendments()}; testWithFeats(all); } diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index a6055d85f6..46b64e40f2 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -1428,7 +1428,7 @@ class MPToken_test : public beast::unit_test::suite testcase("DepositPreauth disabled featureCredentials"); { - Env env(*this, supported_amendments() - featureCredentials); + Env env(*this, testable_amendments() - featureCredentials); std::string const credIdx = "D007AE4B6E1274B4AF872588267B810C2F82716726351D1C7D38D3E5499FC6" @@ -1694,15 +1694,6 @@ class MPToken_test : public beast::unit_test::suite jv[jss::SendMax] = mpt.getJson(JsonOptions::none); test(jv, jss::SendMax.c_str()); } - // EscrowCreate - { - Json::Value jv; - jv[jss::TransactionType] = jss::EscrowCreate; - jv[jss::Account] = alice.human(); - jv[jss::Destination] = carol.human(); - jv[jss::Amount] = mpt.getJson(JsonOptions::none); - test(jv, jss::Amount.c_str()); - } // OfferCreate { Json::Value jv = offer(alice, USD(100), mpt); @@ -2302,7 +2293,7 @@ public: run() override { using namespace test::jtx; - FeatureBitset const all{supported_amendments()}; + FeatureBitset const all{testable_amendments()}; // MPTokenIssuanceCreate testCreateValidation(all - featureSingleAssetVault); diff --git a/src/test/app/MultiSign_test.cpp b/src/test/app/MultiSign_test.cpp index b24c7ca39e..571ec33417 100644 --- a/src/test/app/MultiSign_test.cpp +++ b/src/test/app/MultiSign_test.cpp @@ -460,7 +460,7 @@ public: // Attempt a multisigned transaction that meets the quorum. auto const baseFee = env.current()->fees().base; std::uint32_t aliceSeq = env.seq(alice); - env(noop(alice), msig(msig::Reg{cheri, cher}), fee(2 * baseFee)); + env(noop(alice), msig(Reg{cheri, cher}), fee(2 * baseFee)); env.close(); BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); @@ -480,7 +480,7 @@ public: BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); aliceSeq = env.seq(alice); - env(noop(alice), msig(msig::Reg{becky, beck}), fee(2 * baseFee)); + env(noop(alice), msig(Reg{becky, beck}), fee(2 * baseFee)); env.close(); BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); @@ -488,7 +488,7 @@ public: aliceSeq = env.seq(alice); env(noop(alice), fee(3 * baseFee), - msig(msig::Reg{becky, beck}, msig::Reg{cheri, cher})); + msig(Reg{becky, beck}, Reg{cheri, cher})); env.close(); BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); } @@ -783,12 +783,12 @@ public: BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); aliceSeq = env.seq(alice); - env(noop(alice), msig(msig::Reg{cheri, cher}), fee(2 * baseFee)); + env(noop(alice), msig(Reg{cheri, cher}), fee(2 * baseFee)); env.close(); BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); aliceSeq = env.seq(alice); - env(noop(alice), msig(msig::Reg{daria, dari}), fee(2 * baseFee)); + env(noop(alice), msig(Reg{daria, dari}), fee(2 * baseFee)); env.close(); BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); @@ -801,7 +801,7 @@ public: aliceSeq = env.seq(alice); env(noop(alice), fee(5 * baseFee), - msig(becky, msig::Reg{cheri, cher}, msig::Reg{daria, dari}, jinni)); + msig(becky, Reg{cheri, cher}, Reg{daria, dari}, jinni)); env.close(); BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); @@ -820,7 +820,7 @@ public: aliceSeq = env.seq(alice); env(noop(alice), fee(9 * baseFee), - msig(becky, msig::Reg{cheri, cher}, msig::Reg{daria, dari}, jinni)); + msig(becky, Reg{cheri, cher}, Reg{daria, dari}, jinni)); env.close(); BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); @@ -828,7 +828,7 @@ public: aliceSeq = env.seq(alice); env(noop(alice), fee(5 * baseFee), - msig(becky, cheri, msig::Reg{daria, dari}, jinni)); + msig(becky, cheri, Reg{daria, dari}, jinni)); env.close(); BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); @@ -853,8 +853,8 @@ public: fee(9 * baseFee), msig( becky, - msig::Reg{cheri, cher}, - msig::Reg{daria, dari}, + Reg{cheri, cher}, + Reg{daria, dari}, haunt, jinni, phase, @@ -1349,7 +1349,7 @@ public: // Becky cannot 2-level multisign for alice. 2-level multisigning // is not supported. env(noop(alice), - msig(msig::Reg{becky, bogie}), + msig(Reg{becky, bogie}), fee(2 * baseFee), ter(tefBAD_SIGNATURE)); env.close(); @@ -1358,7 +1358,7 @@ public: // not yet enabled. Account const beck{"beck", KeyType::ed25519}; env(noop(alice), - msig(msig::Reg{becky, beck}), + msig(Reg{becky, beck}), fee(2 * baseFee), ter(tefBAD_SIGNATURE)); env.close(); @@ -1368,13 +1368,13 @@ public: env(regkey(becky, beck), msig(demon), fee(2 * baseFee)); env.close(); - env(noop(alice), msig(msig::Reg{becky, beck}), fee(2 * baseFee)); + env(noop(alice), msig(Reg{becky, beck}), fee(2 * baseFee)); env.close(); // The presence of becky's regular key does not influence whether she // can 2-level multisign; it still won't work. env(noop(alice), - msig(msig::Reg{becky, demon}), + msig(Reg{becky, demon}), fee(2 * baseFee), ter(tefBAD_SIGNATURE)); env.close(); @@ -1478,7 +1478,7 @@ public: Account const cheri{"cheri", KeyType::secp256k1}; Account const daria{"daria", KeyType::ed25519}; - Env env{*this, supported_amendments() - featureMultiSignReserve}; + Env env{*this, testable_amendments() - featureMultiSignReserve}; env.fund(XRP(1000), alice, becky, cheri, daria); env.close(); @@ -1729,7 +1729,7 @@ public: run() override { using namespace jtx; - auto const all = supported_amendments(); + auto const all = testable_amendments(); // The reserve required on a signer list changes based on // featureMultiSignReserve. Limits on the number of signers diff --git a/src/test/app/NFTokenAuth_test.cpp b/src/test/app/NFTokenAuth_test.cpp new file mode 100644 index 0000000000..1a59dc579a --- /dev/null +++ b/src/test/app/NFTokenAuth_test.cpp @@ -0,0 +1,624 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +#include + +namespace ripple { + +class NFTokenAuth_test : public beast::unit_test::suite +{ + auto + mintAndOfferNFT( + test::jtx::Env& env, + test::jtx::Account const& account, + test::jtx::PrettyAmount const& currency, + uint32_t xfee = 0u) + { + using namespace test::jtx; + auto const nftID{ + token::getNextID(env, account, 0u, tfTransferable, xfee)}; + env(token::mint(account, 0), + token::xferFee(xfee), + txflags(tfTransferable)); + env.close(); + + auto const sellIdx = keylet::nftoffer(account, env.seq(account)).key; + env(token::createOffer(account, nftID, currency), + txflags(tfSellNFToken)); + env.close(); + + return std::make_tuple(nftID, sellIdx); + } + +public: + void + testBuyOffer_UnauthorizedSeller(FeatureBitset features) + { + testcase("Unauthorized seller tries to accept buy offer"); + using namespace test::jtx; + + Env env(*this, features); + Account G1{"G1"}; + Account A1{"A1"}; + Account A2{"A2"}; + auto const USD{G1["USD"]}; + + env.fund(XRP(10000), G1, A1, A2); + env(fset(G1, asfRequireAuth)); + env.close(); + + auto const limit = USD(10000); + + env(trust(A1, limit)); + env(trust(G1, limit, A1, tfSetfAuth)); + env(pay(G1, A1, USD(1000))); + + auto const [nftID, _] = mintAndOfferNFT(env, A2, drops(1)); + auto const buyIdx = keylet::nftoffer(A1, env.seq(A1)).key; + + // It should be possible to create a buy offer even if NFT owner is not + // authorized + env(token::createOffer(A1, nftID, USD(10)), token::owner(A2)); + + if (features[fixEnforceNFTokenTrustlineV2]) + { + // test: G1 requires authorization of A2, no trust line exists + env(token::acceptBuyOffer(A2, buyIdx), ter(tecNO_LINE)); + env.close(); + + // trust line created, but not authorized + env(trust(A2, limit)); + + // test: G1 requires authorization of A2 + env(token::acceptBuyOffer(A2, buyIdx), ter(tecNO_AUTH)); + env.close(); + } + else + { + // Old behavior: it is possible to sell tokens and receive IOUs + // without the authorization + env(token::acceptBuyOffer(A2, buyIdx)); + env.close(); + + BEAST_EXPECT(env.balance(A2, USD) == USD(10)); + } + } + + void + testCreateBuyOffer_UnauthorizedBuyer(FeatureBitset features) + { + testcase("Unauthorized buyer tries to create buy offer"); + using namespace test::jtx; + + Env env(*this, features); + Account G1{"G1"}; + Account A1{"A1"}; + Account A2{"A2"}; + auto const USD{G1["USD"]}; + + env.fund(XRP(10000), G1, A1, A2); + env(fset(G1, asfRequireAuth)); + env.close(); + + auto const [nftID, _] = mintAndOfferNFT(env, A2, drops(1)); + + // test: check that buyer can't make an offer if they're not authorized. + env(token::createOffer(A1, nftID, USD(10)), + token::owner(A2), + ter(tecUNFUNDED_OFFER)); + env.close(); + + // Artificially create an unauthorized trustline with balance. Don't + // close ledger before running the actual tests against this trustline. + // After ledger is closed, the trustline will not exist. + auto const unauthTrustline = [&](OpenView& view, + beast::Journal) -> bool { + auto const sleA1 = + std::make_shared(keylet::line(A1, G1, G1["USD"].currency)); + sleA1->setFieldAmount(sfBalance, A1["USD"](-1000)); + view.rawInsert(sleA1); + return true; + }; + env.app().openLedger().modify(unauthTrustline); + + if (features[fixEnforceNFTokenTrustlineV2]) + { + // test: check that buyer can't make an offer even with balance + env(token::createOffer(A1, nftID, USD(10)), + token::owner(A2), + ter(tecNO_AUTH)); + } + else + { + // old behavior: can create an offer if balance allows, regardless + // ot authorization + env(token::createOffer(A1, nftID, USD(10)), token::owner(A2)); + } + } + + void + testAcceptBuyOffer_UnauthorizedBuyer(FeatureBitset features) + { + testcase("Seller tries to accept buy offer from unauth buyer"); + using namespace test::jtx; + + Env env(*this, features); + Account G1{"G1"}; + Account A1{"A1"}; + Account A2{"A2"}; + auto const USD{G1["USD"]}; + + env.fund(XRP(10000), G1, A1, A2); + env(fset(G1, asfRequireAuth)); + env.close(); + + auto const limit = USD(10000); + + auto const [nftID, _] = mintAndOfferNFT(env, A2, drops(1)); + + // First we authorize buyer and seller so that he can create buy offer + env(trust(A1, limit)); + env(trust(G1, limit, A1, tfSetfAuth)); + env(pay(G1, A1, USD(10))); + env(trust(A2, limit)); + env(trust(G1, limit, A2, tfSetfAuth)); + env(pay(G1, A2, USD(10))); + env.close(); + + auto const buyIdx = keylet::nftoffer(A1, env.seq(A1)).key; + env(token::createOffer(A1, nftID, USD(10)), token::owner(A2)); + env.close(); + + env(pay(A1, G1, USD(10))); + env(trust(A1, USD(0))); + env(trust(G1, A1["USD"](0))); + env.close(); + + // Replace an existing authorized trustline with artificial unauthorized + // trustline with balance. Don't close ledger before running the actual + // tests against this trustline. After ledger is closed, the trustline + // will not exist. + auto const unauthTrustline = [&](OpenView& view, + beast::Journal) -> bool { + auto const sleA1 = + std::make_shared(keylet::line(A1, G1, G1["USD"].currency)); + sleA1->setFieldAmount(sfBalance, A1["USD"](-1000)); + view.rawInsert(sleA1); + return true; + }; + env.app().openLedger().modify(unauthTrustline); + if (features[fixEnforceNFTokenTrustlineV2]) + { + // test: check that offer can't be accepted even with balance + env(token::acceptBuyOffer(A2, buyIdx), ter(tecNO_AUTH)); + } + } + + void + testSellOffer_UnauthorizedSeller(FeatureBitset features) + { + testcase( + "Authorized buyer tries to accept sell offer from unauthorized " + "seller"); + using namespace test::jtx; + + Env env(*this, features); + Account G1{"G1"}; + Account A1{"A1"}; + Account A2{"A2"}; + auto const USD{G1["USD"]}; + + env.fund(XRP(10000), G1, A1, A2); + env(fset(G1, asfRequireAuth)); + env.close(); + + auto const limit = USD(10000); + + env(trust(A1, limit)); + env(trust(G1, limit, A1, tfSetfAuth)); + env(pay(G1, A1, USD(1000))); + + auto const [nftID, _] = mintAndOfferNFT(env, A2, drops(1)); + if (features[fixEnforceNFTokenTrustlineV2]) + { + // test: can't create sell offer if there is no trustline but auth + // required + env(token::createOffer(A2, nftID, USD(10)), + txflags(tfSellNFToken), + ter(tecNO_LINE)); + + env(trust(A2, limit)); + // test: can't create sell offer if not authorized to hold token + env(token::createOffer(A2, nftID, USD(10)), + txflags(tfSellNFToken), + ter(tecNO_AUTH)); + + // Authorizing trustline to make an offer creation possible + env(trust(G1, USD(0), A2, tfSetfAuth)); + env.close(); + auto const sellIdx = keylet::nftoffer(A2, env.seq(A2)).key; + env(token::createOffer(A2, nftID, USD(10)), txflags(tfSellNFToken)); + env.close(); + // + + // Reseting trustline to delete it. This allows to check if + // already existing offers handled correctly + env(trust(A2, USD(0))); + env.close(); + + // test: G1 requires authorization of A1, no trust line exists + env(token::acceptSellOffer(A1, sellIdx), ter(tecNO_LINE)); + env.close(); + + // trust line created, but not authorized + env(trust(A2, limit)); + env.close(); + + // test: G1 requires authorization of A1 + env(token::acceptSellOffer(A1, sellIdx), ter(tecNO_AUTH)); + env.close(); + } + else + { + auto const sellIdx = keylet::nftoffer(A2, env.seq(A2)).key; + + // Old behavior: sell offer can be created without authorization + env(token::createOffer(A2, nftID, USD(10)), txflags(tfSellNFToken)); + env.close(); + + // Old behavior: it is possible to sell NFT and receive IOUs + // without the authorization + env(token::acceptSellOffer(A1, sellIdx)); + env.close(); + + BEAST_EXPECT(env.balance(A2, USD) == USD(10)); + } + } + + void + testSellOffer_UnauthorizedBuyer(FeatureBitset features) + { + testcase("Unauthorized buyer tries to accept sell offer"); + using namespace test::jtx; + + Env env(*this, features); + Account G1{"G1"}; + Account A1{"A1"}; + Account A2{"A2"}; + auto const USD{G1["USD"]}; + + env.fund(XRP(10000), G1, A1, A2); + env(fset(G1, asfRequireAuth)); + env.close(); + + auto const limit = USD(10000); + + env(trust(A2, limit)); + env(trust(G1, limit, A2, tfSetfAuth)); + + auto const [_, sellIdx] = mintAndOfferNFT(env, A2, USD(10)); + + // test: check that buyer can't accept an offer if they're not + // authorized. + env(token::acceptSellOffer(A1, sellIdx), ter(tecINSUFFICIENT_FUNDS)); + env.close(); + + // Creating an artificial unauth trustline + auto const unauthTrustline = [&](OpenView& view, + beast::Journal) -> bool { + auto const sleA1 = + std::make_shared(keylet::line(A1, G1, G1["USD"].currency)); + sleA1->setFieldAmount(sfBalance, A1["USD"](-1000)); + view.rawInsert(sleA1); + return true; + }; + env.app().openLedger().modify(unauthTrustline); + if (features[fixEnforceNFTokenTrustlineV2]) + { + env(token::acceptSellOffer(A1, sellIdx), ter(tecNO_AUTH)); + } + } + + void + testBrokeredAcceptOffer_UnauthorizedBroker(FeatureBitset features) + { + testcase("Unauthorized broker bridges authorized buyer and seller."); + using namespace test::jtx; + + Env env(*this, features); + Account G1{"G1"}; + Account A1{"A1"}; + Account A2{"A2"}; + Account broker{"broker"}; + auto const USD{G1["USD"]}; + + env.fund(XRP(10000), G1, A1, A2, broker); + env(fset(G1, asfRequireAuth)); + env.close(); + + auto const limit = USD(10000); + + env(trust(A1, limit)); + env(trust(G1, limit, A1, tfSetfAuth)); + env(pay(G1, A1, USD(1000))); + env(trust(A2, limit)); + env(trust(G1, limit, A2, tfSetfAuth)); + env(pay(G1, A2, USD(1000))); + env.close(); + + auto const [nftID, sellIdx] = mintAndOfferNFT(env, A2, USD(10)); + auto const buyIdx = keylet::nftoffer(A1, env.seq(A1)).key; + env(token::createOffer(A1, nftID, USD(11)), token::owner(A2)); + env.close(); + + if (features[fixEnforceNFTokenTrustlineV2]) + { + // test: G1 requires authorization of broker, no trust line exists + env(token::brokerOffers(broker, buyIdx, sellIdx), + token::brokerFee(USD(1)), + ter(tecNO_LINE)); + env.close(); + + // trust line created, but not authorized + env(trust(broker, limit)); + env.close(); + + // test: G1 requires authorization of broker + env(token::brokerOffers(broker, buyIdx, sellIdx), + token::brokerFee(USD(1)), + ter(tecNO_AUTH)); + env.close(); + + // test: can still be brokered without broker fee. + env(token::brokerOffers(broker, buyIdx, sellIdx)); + env.close(); + } + else + { + // Old behavior: broker can receive IOUs without the authorization + env(token::brokerOffers(broker, buyIdx, sellIdx), + token::brokerFee(USD(1))); + env.close(); + + BEAST_EXPECT(env.balance(broker, USD) == USD(1)); + } + } + + void + testBrokeredAcceptOffer_UnauthorizedBuyer(FeatureBitset features) + { + testcase( + "Authorized broker tries to bridge offers from unauthorized " + "buyer."); + using namespace test::jtx; + + Env env(*this, features); + Account G1{"G1"}; + Account A1{"A1"}; + Account A2{"A2"}; + Account broker{"broker"}; + auto const USD{G1["USD"]}; + + env.fund(XRP(10000), G1, A1, A2, broker); + env(fset(G1, asfRequireAuth)); + env.close(); + + auto const limit = USD(10000); + + env(trust(A1, limit)); + env(trust(G1, USD(0), A1, tfSetfAuth)); + env(pay(G1, A1, USD(1000))); + env(trust(A2, limit)); + env(trust(G1, USD(0), A2, tfSetfAuth)); + env(pay(G1, A2, USD(1000))); + env(trust(broker, limit)); + env(trust(G1, USD(0), broker, tfSetfAuth)); + env(pay(G1, broker, USD(1000))); + env.close(); + + auto const [nftID, sellIdx] = mintAndOfferNFT(env, A2, USD(10)); + auto const buyIdx = keylet::nftoffer(A1, env.seq(A1)).key; + env(token::createOffer(A1, nftID, USD(11)), token::owner(A2)); + env.close(); + + // Resetting buyer's trust line to delete it + env(pay(A1, G1, USD(1000))); + env(trust(A1, USD(0))); + env.close(); + + auto const unauthTrustline = [&](OpenView& view, + beast::Journal) -> bool { + auto const sleA1 = + std::make_shared(keylet::line(A1, G1, G1["USD"].currency)); + sleA1->setFieldAmount(sfBalance, A1["USD"](-1000)); + view.rawInsert(sleA1); + return true; + }; + env.app().openLedger().modify(unauthTrustline); + + if (features[fixEnforceNFTokenTrustlineV2]) + { + // test: G1 requires authorization of A2 + env(token::brokerOffers(broker, buyIdx, sellIdx), + token::brokerFee(USD(1)), + ter(tecNO_AUTH)); + env.close(); + } + } + + void + testBrokeredAcceptOffer_UnauthorizedSeller(FeatureBitset features) + { + testcase( + "Authorized broker tries to bridge offers from unauthorized " + "seller."); + using namespace test::jtx; + + Env env(*this, features); + Account G1{"G1"}; + Account A1{"A1"}; + Account A2{"A2"}; + Account broker{"broker"}; + auto const USD{G1["USD"]}; + + env.fund(XRP(10000), G1, A1, A2, broker); + env(fset(G1, asfRequireAuth)); + env.close(); + + auto const limit = USD(10000); + + env(trust(A1, limit)); + env(trust(G1, limit, A1, tfSetfAuth)); + env(pay(G1, A1, USD(1000))); + env(trust(broker, limit)); + env(trust(G1, limit, broker, tfSetfAuth)); + env(pay(G1, broker, USD(1000))); + env.close(); + + // Authorizing trustline to make an offer creation possible + env(trust(G1, USD(0), A2, tfSetfAuth)); + env.close(); + + auto const [nftID, sellIdx] = mintAndOfferNFT(env, A2, USD(10)); + auto const buyIdx = keylet::nftoffer(A1, env.seq(A1)).key; + env(token::createOffer(A1, nftID, USD(11)), token::owner(A2)); + env.close(); + + // Reseting trustline to delete it. This allows to check if + // already existing offers handled correctly + env(trust(A2, USD(0))); + env.close(); + + if (features[fixEnforceNFTokenTrustlineV2]) + { + // test: G1 requires authorization of broker, no trust line exists + env(token::brokerOffers(broker, buyIdx, sellIdx), + token::brokerFee(USD(1)), + ter(tecNO_LINE)); + env.close(); + + // trust line created, but not authorized + env(trust(A2, limit)); + env.close(); + + // test: G1 requires authorization of A2 + env(token::brokerOffers(broker, buyIdx, sellIdx), + token::brokerFee(USD(1)), + ter(tecNO_AUTH)); + env.close(); + + // test: cannot be brokered even without broker fee. + env(token::brokerOffers(broker, buyIdx, sellIdx), ter(tecNO_AUTH)); + env.close(); + } + else + { + // Old behavior: broker can receive IOUs without the authorization + env(token::brokerOffers(broker, buyIdx, sellIdx), + token::brokerFee(USD(1))); + env.close(); + + BEAST_EXPECT(env.balance(A2, USD) == USD(10)); + return; + } + } + + void + testTransferFee_UnauthorizedMinter(FeatureBitset features) + { + testcase("Unauthorized minter receives transfer fee."); + using namespace test::jtx; + + Env env(*this, features); + Account G1{"G1"}; + Account minter{"minter"}; + Account A1{"A1"}; + Account A2{"A2"}; + auto const USD{G1["USD"]}; + + env.fund(XRP(10000), G1, minter, A1, A2); + env(fset(G1, asfRequireAuth)); + env.close(); + + auto const limit = USD(10000); + + env(trust(A1, limit)); + env(trust(G1, limit, A1, tfSetfAuth)); + env(pay(G1, A1, USD(1000))); + env(trust(A2, limit)); + env(trust(G1, limit, A2, tfSetfAuth)); + env(pay(G1, A2, USD(1000))); + + env(trust(minter, limit)); + env.close(); + + // We authorized A1 and A2, but not the minter. + // Now mint NFT + auto const [nftID, minterSellIdx] = + mintAndOfferNFT(env, minter, drops(1), 1); + env(token::acceptSellOffer(A1, minterSellIdx)); + + uint256 const sellIdx = keylet::nftoffer(A1, env.seq(A1)).key; + env(token::createOffer(A1, nftID, USD(100)), txflags(tfSellNFToken)); + + if (features[fixEnforceNFTokenTrustlineV2]) + { + // test: G1 requires authorization + env(token::acceptSellOffer(A2, sellIdx), ter(tecNO_AUTH)); + env.close(); + } + else + { + // Old behavior: can sell for USD. Minter can receive tokens + env(token::acceptSellOffer(A2, sellIdx)); + env.close(); + + BEAST_EXPECT(env.balance(minter, USD) == USD(0.001)); + } + } + + void + run() override + { + using namespace test::jtx; + static FeatureBitset const all{testable_amendments()}; + + static std::array const features = { + all - fixEnforceNFTokenTrustlineV2, all}; + + for (auto const feature : features) + { + testBuyOffer_UnauthorizedSeller(feature); + testCreateBuyOffer_UnauthorizedBuyer(feature); + testAcceptBuyOffer_UnauthorizedBuyer(feature); + testSellOffer_UnauthorizedSeller(feature); + testSellOffer_UnauthorizedBuyer(feature); + testBrokeredAcceptOffer_UnauthorizedBroker(feature); + testBrokeredAcceptOffer_UnauthorizedBuyer(feature); + testBrokeredAcceptOffer_UnauthorizedSeller(feature); + testTransferFee_UnauthorizedMinter(feature); + } + } +}; + +BEAST_DEFINE_TESTSUITE_PRIO(NFTokenAuth, tx, ripple, 2); + +} // namespace ripple \ No newline at end of file diff --git a/src/test/app/NFTokenBurn_test.cpp b/src/test/app/NFTokenBurn_test.cpp index a970b11789..21b0a1ffd8 100644 --- a/src/test/app/NFTokenBurn_test.cpp +++ b/src/test/app/NFTokenBurn_test.cpp @@ -1385,7 +1385,7 @@ protected: run(std::uint32_t instance, bool last = false) { using namespace test::jtx; - static FeatureBitset const all{supported_amendments()}; + static FeatureBitset const all{testable_amendments()}; static FeatureBitset const fixNFTV1_2{fixNonFungibleTokensV1_2}; static FeatureBitset const fixNFTDir{fixNFTokenDirV1}; static FeatureBitset const fixNFTRemint{fixNFTokenRemint}; diff --git a/src/test/app/NFTokenDir_test.cpp b/src/test/app/NFTokenDir_test.cpp index fe21e02739..df40781590 100644 --- a/src/test/app/NFTokenDir_test.cpp +++ b/src/test/app/NFTokenDir_test.cpp @@ -1100,7 +1100,7 @@ public: run() override { using namespace test::jtx; - FeatureBitset const all{supported_amendments()}; + FeatureBitset const all{testable_amendments()}; FeatureBitset const fixNFTDir{ fixNFTokenDirV1, featureNonFungibleTokensV1_1}; diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index 41bcc673d5..b79ebf3c40 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -8075,7 +8075,7 @@ public: run(std::uint32_t instance, bool last = false) { using namespace test::jtx; - static FeatureBitset const all{supported_amendments()}; + static FeatureBitset const all{testable_amendments()}; static FeatureBitset const fixNFTDir{fixNFTokenDirV1}; static std::array const feats{ diff --git a/src/test/app/Offer_test.cpp b/src/test/app/Offer_test.cpp index 4da8d8101e..96f68fb2ad 100644 --- a/src/test/app/Offer_test.cpp +++ b/src/test/app/Offer_test.cpp @@ -1343,18 +1343,10 @@ public: // NOTE : // At this point, all offers are expected to be consumed. - // Alas, they are not - because of a bug in the Taker auto-bridging - // implementation which is addressed by fixTakerDryOfferRemoval. - // The pre-fixTakerDryOfferRemoval implementation (incorrect) leaves - // an empty offer in the second leg of the bridge. Validate both the - // old and the new behavior. { auto acctOffers = offersOnAccount(env, account_to_test); - bool const noStaleOffers{ - features[featureFlowCross] || - features[fixTakerDryOfferRemoval]}; - BEAST_EXPECT(acctOffers.size() == (noStaleOffers ? 0 : 1)); + BEAST_EXPECT(acctOffers.size() == 0); for (auto const& offerPtr : acctOffers) { auto const& offer = *offerPtr; @@ -1464,8 +1456,7 @@ public: std::uint32_t const bobOfferSeq = env.seq(bob); env(offer(bob, XRP(2000), USD(1))); - if (localFeatures[featureFlowCross] && - localFeatures[fixReducedOffersV2]) + if (localFeatures[fixReducedOffersV2]) { // With the rounding introduced by fixReducedOffersV2, bob's // offer does not cross alice's offer and goes straight into @@ -1489,8 +1480,7 @@ public: // crossing algorithms becomes apparent. The old offer crossing // would consume small_amount and transfer no XRP. The new offer // crossing transfers a single drop, rather than no drops. - auto const crossingDelta = - localFeatures[featureFlowCross] ? drops(1) : drops(0); + auto const crossingDelta = drops(1); jrr = ledgerEntryState(env, alice, gw, "USD"); BEAST_EXPECT( @@ -2044,15 +2034,9 @@ public: env.require(balance(carol, USD(0))); env.require(balance(carol, EUR(none))); - // If neither featureFlowCross nor fixTakerDryOfferRemoval are defined - // then carol's offer will be left on the books, but with zero value. - int const emptyOfferCount{ - features[featureFlowCross] || features[fixTakerDryOfferRemoval] - ? 0 - : 1}; - env.require(offers(carol, 0 + emptyOfferCount)); - env.require(owners(carol, 1 + emptyOfferCount)); + env.require(offers(carol, 0)); + env.require(owners(carol, 1)); } void @@ -3643,9 +3627,7 @@ public: using namespace jtx; - // The problem was identified when featureOwnerPaysFee was enabled, - // so make sure that gets included. - Env env{*this, features | featureOwnerPaysFee}; + Env env{*this, features}; // The fee that's charged for transactions. auto const fee = env.current()->fees().base; @@ -4238,22 +4220,13 @@ public: }; // clang-format off - TestData const takerTests[]{ - // btcStart ------------------- actor[0] -------------------- ------------------- actor[1] -------------------- - {0, 0, 1, BTC(5), {{"deb", 0, drops(3900000'000000 - 4 * baseFee), BTC(5), USD(3000)}, {"dan", 0, drops(4100000'000000 - 3 * baseFee), BTC(0), USD(750)}}}, // no BTC xfer fee - {0, 0, 0, BTC(5), {{"flo", 0, drops(4000000'000000 - 5 * baseFee), BTC(5), USD(2000)} }} // no xfer fee - }; - - TestData const flowTests[]{ + TestData const tests[]{ // btcStart ------------------- actor[0] -------------------- ------------------- actor[1] -------------------- {0, 0, 1, BTC(5), {{"gay", 1, drops(3950000'000000 - 4 * baseFee), BTC(5), USD(2500)}, {"gar", 1, drops(4050000'000000 - 3 * baseFee), BTC(0), USD(1375)}}}, // no BTC xfer fee {0, 0, 0, BTC(5), {{"hye", 2, drops(4000000'000000 - 5 * baseFee), BTC(5), USD(2000)} }} // no xfer fee }; // clang-format on - // Pick the right tests. - auto const& tests = features[featureFlowCross] ? flowTests : takerTests; - for (auto const& t : tests) { Account const& self = t.actors[t.self].acct; @@ -4380,9 +4353,8 @@ public: // 1. alice creates an offer to acquire USD/gw, an asset for which // she does not have a trust line. At some point in the future, // gw adds lsfRequireAuth. Then, later, alice's offer is crossed. - // a. With Taker alice's unauthorized offer is consumed. - // b. With FlowCross alice's offer is deleted, not consumed, - // since alice is not authorized to hold USD/gw. + // Alice's offer is deleted, not consumed, since alice is not + // authorized to hold USD/gw. // // 2. alice tries to create an offer for USD/gw, now that gw has // lsfRequireAuth set. This time the offer create fails because @@ -4430,33 +4402,17 @@ public: // gw now requires authorization and bob has gwUSD(50). Let's see if // bob can cross alice's offer. // - // o With Taker bob's offer should cross alice's. - // o With FlowCross bob's offer shouldn't cross and alice's - // unauthorized offer should be deleted. + // Bob's offer shouldn't cross and alice's unauthorized offer should be + // deleted. env(offer(bob, XRP(4000), gwUSD(40))); env.close(); std::uint32_t const bobOfferSeq = env.seq(bob) - 1; - bool const flowCross = features[featureFlowCross]; - env.require(offers(alice, 0)); - if (flowCross) - { - // alice's unauthorized offer is deleted & bob's offer not crossed. - env.require(balance(alice, gwUSD(none))); - env.require(offers(bob, 1)); - env.require(balance(bob, gwUSD(50))); - } - else - { - // alice's offer crosses bob's - env.require(balance(alice, gwUSD(40))); - env.require(offers(bob, 0)); - env.require(balance(bob, gwUSD(10))); - - // The rest of the test verifies FlowCross behavior. - return; - } + // alice's unauthorized offer is deleted & bob's offer not crossed. + env.require(balance(alice, gwUSD(none))); + env.require(offers(bob, 1)); + env.require(balance(bob, gwUSD(50))); // See if alice can create an offer without authorization. alice // should not be able to create the offer and bob's offer should be @@ -5188,9 +5144,7 @@ public: // tfFillOrKill, TakerPays must be filled { TER const err = - features[fixFillOrKill] || !features[featureFlowCross] - ? TER(tesSUCCESS) - : tecKILLED; + features[fixFillOrKill] ? TER(tesSUCCESS) : tecKILLED; env(offer(maker, XRP(100), USD(100))); env.close(); @@ -5411,21 +5365,22 @@ public: run(std::uint32_t instance, bool last = false) { using namespace jtx; - static FeatureBitset const all{supported_amendments()}; - static FeatureBitset const flowCross{featureFlowCross}; + static FeatureBitset const all{testable_amendments()}; static FeatureBitset const takerDryOffer{fixTakerDryOfferRemoval}; static FeatureBitset const rmSmallIncreasedQOffers{ fixRmSmallIncreasedQOffers}; static FeatureBitset const immediateOfferKilled{ featureImmediateOfferKilled}; FeatureBitset const fillOrKill{fixFillOrKill}; + FeatureBitset const permDEX{featurePermissionedDEX}; static std::array const feats{ - all - takerDryOffer - immediateOfferKilled, - all - flowCross - takerDryOffer - immediateOfferKilled, - all - flowCross - immediateOfferKilled, - all - rmSmallIncreasedQOffers - immediateOfferKilled - fillOrKill, - all - fillOrKill, + all - takerDryOffer - immediateOfferKilled - permDEX, + all - immediateOfferKilled - permDEX, + all - rmSmallIncreasedQOffers - immediateOfferKilled - fillOrKill - + permDEX, + all - fillOrKill - permDEX, + all - permDEX, all}; if (BEAST_EXPECT(instance < feats.size())) @@ -5443,7 +5398,7 @@ public: } }; -class OfferWOFlowCross_test : public OfferBaseUtil_test +class OfferWTakerDryOffer_test : public OfferBaseUtil_test { void run() override @@ -5452,7 +5407,7 @@ class OfferWOFlowCross_test : public OfferBaseUtil_test } }; -class OfferWTakerDryOffer_test : public OfferBaseUtil_test +class OfferWOSmallQOffers_test : public OfferBaseUtil_test { void run() override @@ -5461,7 +5416,7 @@ class OfferWTakerDryOffer_test : public OfferBaseUtil_test } }; -class OfferWOSmallQOffers_test : public OfferBaseUtil_test +class OfferWOFillOrKill_test : public OfferBaseUtil_test { void run() override @@ -5470,7 +5425,7 @@ class OfferWOSmallQOffers_test : public OfferBaseUtil_test } }; -class OfferWOFillOrKill_test : public OfferBaseUtil_test +class OfferWOPermDEX_test : public OfferBaseUtil_test { void run() override @@ -5494,28 +5449,28 @@ class Offer_manual_test : public OfferBaseUtil_test run() override { using namespace jtx; - FeatureBitset const all{supported_amendments()}; - FeatureBitset const flowCross{featureFlowCross}; + FeatureBitset const all{testable_amendments()}; FeatureBitset const f1513{fix1513}; FeatureBitset const immediateOfferKilled{featureImmediateOfferKilled}; FeatureBitset const takerDryOffer{fixTakerDryOfferRemoval}; FeatureBitset const fillOrKill{fixFillOrKill}; + FeatureBitset const permDEX{featurePermissionedDEX}; - testAll(all - flowCross - f1513 - immediateOfferKilled); - testAll(all - flowCross - immediateOfferKilled); - testAll(all - immediateOfferKilled - fillOrKill); - testAll(all - fillOrKill); + testAll(all - f1513 - immediateOfferKilled - permDEX); + testAll(all - immediateOfferKilled - fillOrKill - permDEX); + testAll(all - fillOrKill - permDEX); + testAll(all - permDEX); testAll(all); - testAll(all - flowCross - takerDryOffer); + testAll(all - takerDryOffer - permDEX); } }; BEAST_DEFINE_TESTSUITE_PRIO(OfferBaseUtil, tx, ripple, 2); -BEAST_DEFINE_TESTSUITE_PRIO(OfferWOFlowCross, tx, ripple, 2); BEAST_DEFINE_TESTSUITE_PRIO(OfferWTakerDryOffer, tx, ripple, 2); BEAST_DEFINE_TESTSUITE_PRIO(OfferWOSmallQOffers, tx, ripple, 2); BEAST_DEFINE_TESTSUITE_PRIO(OfferWOFillOrKill, tx, ripple, 2); +BEAST_DEFINE_TESTSUITE_PRIO(OfferWOPermDEX, tx, ripple, 2); BEAST_DEFINE_TESTSUITE_PRIO(OfferAllFeatures, tx, ripple, 2); BEAST_DEFINE_TESTSUITE_MANUAL_PRIO(Offer_manual, tx, ripple, 20); diff --git a/src/test/app/Oracle_test.cpp b/src/test/app/Oracle_test.cpp index a968970395..aaa7f9a746 100644 --- a/src/test/app/Oracle_test.cpp +++ b/src/test/app/Oracle_test.cpp @@ -783,7 +783,7 @@ private: testcase("Amendment"); using namespace jtx; - auto const features = supported_amendments() - featurePriceOracle; + auto const features = testable_amendments() - featurePriceOracle; Account const owner("owner"); Env env(*this, features); auto const baseFee = @@ -806,7 +806,7 @@ public: run() override { using namespace jtx; - auto const all = supported_amendments(); + auto const all = testable_amendments(); testInvalidSet(); testInvalidDelete(); testCreate(); diff --git a/src/test/app/Path_test.cpp b/src/test/app/Path_test.cpp index f325b0d2be..6ff22a5dc7 100644 --- a/src/test/app/Path_test.cpp +++ b/src/test/app/Path_test.cpp @@ -18,11 +18,12 @@ //============================================================================== #include +#include +#include #include +#include -#include #include -#include #include #include #include @@ -34,7 +35,12 @@ #include #include +#include +#include #include +#include +#include +#include namespace ripple { namespace test { @@ -126,7 +132,8 @@ public: jtx::Account const& dst, STAmount const& saDstAmount, std::optional const& saSendMax = std::nullopt, - std::optional const& saSrcCurrency = std::nullopt) + std::optional const& saSrcCurrency = std::nullopt, + std::optional const& domain = std::nullopt) { using namespace jtx; @@ -163,6 +170,8 @@ public: j[jss::currency] = to_string(saSrcCurrency.value()); sc.append(j); } + if (domain) + params[jss::domain] = to_string(*domain); Json::Value result; gate g; @@ -187,10 +196,11 @@ public: jtx::Account const& dst, STAmount const& saDstAmount, std::optional const& saSendMax = std::nullopt, - std::optional const& saSrcCurrency = std::nullopt) + std::optional const& saSrcCurrency = std::nullopt, + std::optional const& domain = std::nullopt) { Json::Value result = find_paths_request( - env, src, dst, saDstAmount, saSendMax, saSrcCurrency); + env, src, dst, saDstAmount, saSendMax, saSrcCurrency, domain); BEAST_EXPECT(!result.isMember(jss::error)); STAmount da; @@ -363,9 +373,11 @@ public: } void - path_find() + path_find(bool const domainEnabled) { - testcase("path find"); + testcase( + std::string("path find") + (domainEnabled ? " w/ " : " w/o ") + + "domain"); using namespace jtx; Env env = pathTestEnv(); auto const gw = Account("gateway"); @@ -377,31 +389,50 @@ public: env(pay(gw, "alice", USD(70))); env(pay(gw, "bob", USD(50))); + std::optional domainID; + if (domainEnabled) + domainID = setupDomain(env, {"alice", "bob", gw}); + STPathSet st; STAmount sa; - std::tie(st, sa, std::ignore) = - find_paths(env, "alice", "bob", Account("bob")["USD"](5)); + std::tie(st, sa, std::ignore) = find_paths( + env, + "alice", + "bob", + Account("bob")["USD"](5), + std::nullopt, + std::nullopt, + domainID); BEAST_EXPECT(same(st, stpath("gateway"))); BEAST_EXPECT(equal(sa, Account("alice")["USD"](5))); } void - xrp_to_xrp() + xrp_to_xrp(bool const domainEnabled) { using namespace jtx; - testcase("XRP to XRP"); + testcase( + std::string("XRP to XRP") + (domainEnabled ? " w/ " : " w/o ") + + "domain"); Env env = pathTestEnv(); env.fund(XRP(10000), "alice", "bob"); env.close(); - auto const result = find_paths(env, "alice", "bob", XRP(5)); + std::optional domainID; + if (domainEnabled) + domainID = setupDomain(env, {"alice", "bob"}); + + auto const result = find_paths( + env, "alice", "bob", XRP(5), std::nullopt, std::nullopt, domainID); BEAST_EXPECT(std::get<0>(result).empty()); } void - path_find_consume_all() + path_find_consume_all(bool const domainEnabled) { - testcase("path find consume all"); + testcase( + std::string("path find consume all") + + (domainEnabled ? " w/ " : " w/o ") + "domain"); using namespace jtx; { @@ -414,11 +445,22 @@ public: env.trust(Account("alice")["USD"](100), "dan"); env.trust(Account("dan")["USD"](100), "edward"); + std::optional domainID; + if (domainEnabled) + domainID = setupDomain( + env, {"alice", "bob", "carol", "dan", "edward"}); + STPathSet st; STAmount sa; STAmount da; std::tie(st, sa, da) = find_paths( - env, "alice", "edward", Account("edward")["USD"](-1)); + env, + "alice", + "edward", + Account("edward")["USD"](-1), + std::nullopt, + std::nullopt, + domainID); BEAST_EXPECT(same(st, stpath("dan"), stpath("bob", "carol"))); BEAST_EXPECT(equal(sa, Account("alice")["USD"](110))); BEAST_EXPECT(equal(da, Account("edward")["USD"](110))); @@ -431,8 +473,22 @@ public: env.fund(XRP(10000), "alice", "bob", "carol", gw); env.close(); env.trust(USD(100), "bob", "carol"); + env.close(); env(pay(gw, "carol", USD(100))); - env(offer("carol", XRP(100), USD(100))); + env.close(); + + std::optional domainID; + if (domainEnabled) + { + domainID = + setupDomain(env, {"alice", "bob", "carol", "gateway"}); + env(offer("carol", XRP(100), USD(100)), domain(*domainID)); + } + else + { + env(offer("carol", XRP(100), USD(100))); + } + env.close(); STPathSet st; STAmount sa; @@ -442,23 +498,44 @@ public: "alice", "bob", Account("bob")["AUD"](-1), - std::optional(XRP(100000000))); + std::optional(XRP(1000000)), + std::nullopt, + domainID); BEAST_EXPECT(st.empty()); std::tie(st, sa, da) = find_paths( env, "alice", "bob", Account("bob")["USD"](-1), - std::optional(XRP(100000000))); + std::optional(XRP(1000000)), + std::nullopt, + domainID); BEAST_EXPECT(sa == XRP(100)); BEAST_EXPECT(equal(da, Account("bob")["USD"](100))); + + // if domain is used, finding path in the open offerbook will return + // empty result + if (domainEnabled) + { + std::tie(st, sa, da) = find_paths( + env, + "alice", + "bob", + Account("bob")["USD"](-1), + std::optional(XRP(1000000)), + std::nullopt, + std::nullopt); // not specifying a domain + BEAST_EXPECT(st.empty()); + } } } void - alternative_path_consume_both() + alternative_path_consume_both(bool const domainEnabled) { - testcase("alternative path consume both"); + testcase( + std::string("alternative path consume both") + + (domainEnabled ? " w/ " : " w/o ") + "domain"); using namespace jtx; Env env = pathTestEnv(); auto const gw = Account("gateway"); @@ -471,10 +548,26 @@ public: env.trust(gw2_USD(800), "alice"); env.trust(USD(700), "bob"); env.trust(gw2_USD(900), "bob"); - env(pay(gw, "alice", USD(70))); - env(pay(gw2, "alice", gw2_USD(70))); - env(pay("alice", "bob", Account("bob")["USD"](140)), - paths(Account("alice")["USD"])); + + std::optional domainID; + if (domainEnabled) + { + domainID = + setupDomain(env, {"alice", "bob", "gateway", "gateway2"}); + env(pay(gw, "alice", USD(70)), domain(*domainID)); + env(pay(gw2, "alice", gw2_USD(70)), domain(*domainID)); + env(pay("alice", "bob", Account("bob")["USD"](140)), + paths(Account("alice")["USD"]), + domain(*domainID)); + } + else + { + env(pay(gw, "alice", USD(70))); + env(pay(gw2, "alice", gw2_USD(70))); + env(pay("alice", "bob", Account("bob")["USD"](140)), + paths(Account("alice")["USD"])); + } + env.require(balance("alice", USD(0))); env.require(balance("alice", gw2_USD(0))); env.require(balance("bob", USD(70))); @@ -486,9 +579,11 @@ public: } void - alternative_paths_consume_best_transfer() + alternative_paths_consume_best_transfer(bool const domainEnabled) { - testcase("alternative paths consume best transfer"); + testcase( + std::string("alternative paths consume best transfer") + + (domainEnabled ? " w/ " : " w/o ") + "domain"); using namespace jtx; Env env = pathTestEnv(); auto const gw = Account("gateway"); @@ -502,9 +597,22 @@ public: env.trust(gw2_USD(800), "alice"); env.trust(USD(700), "bob"); env.trust(gw2_USD(900), "bob"); - env(pay(gw, "alice", USD(70))); - env(pay(gw2, "alice", gw2_USD(70))); - env(pay("alice", "bob", USD(70))); + + std::optional domainID; + if (domainEnabled) + { + domainID = + setupDomain(env, {"alice", "bob", "gateway", "gateway2"}); + env(pay(gw, "alice", USD(70)), domain(*domainID)); + env(pay(gw2, "alice", gw2_USD(70)), domain(*domainID)); + env(pay("alice", "bob", USD(70)), domain(*domainID)); + } + else + { + env(pay(gw, "alice", USD(70))); + env(pay(gw2, "alice", gw2_USD(70))); + env(pay("alice", "bob", USD(70))); + } env.require(balance("alice", USD(0))); env.require(balance("alice", gw2_USD(70))); env.require(balance("bob", USD(70))); @@ -548,9 +656,13 @@ public: } void - alternative_paths_limit_returned_paths_to_best_quality() + alternative_paths_limit_returned_paths_to_best_quality( + bool const domainEnabled) { - testcase("alternative paths - limit returned paths to best quality"); + testcase( + std::string( + "alternative paths - limit returned paths to best quality") + + (domainEnabled ? " w/ " : " w/o ") + "domain"); using namespace jtx; Env env = pathTestEnv(); auto const gw = Account("gateway"); @@ -566,14 +678,31 @@ public: env.trust(gw2_USD(800), "alice", "bob"); env.trust(Account("alice")["USD"](800), "dan"); env.trust(Account("bob")["USD"](800), "dan"); + env.close(); env(pay(gw2, "alice", gw2_USD(100))); + env.close(); env(pay("carol", "alice", Account("carol")["USD"](100))); + env.close(); env(pay(gw, "alice", USD(100))); + env.close(); + + std::optional domainID; + if (domainEnabled) + { + domainID = + setupDomain(env, {"alice", "bob", "carol", "dan", gw, gw2}); + } STPathSet st; STAmount sa; - std::tie(st, sa, std::ignore) = - find_paths(env, "alice", "bob", Account("bob")["USD"](5)); + std::tie(st, sa, std::ignore) = find_paths( + env, + "alice", + "bob", + Account("bob")["USD"](5), + std::nullopt, + std::nullopt, + domainID); BEAST_EXPECT(same( st, stpath("gateway"), @@ -584,9 +713,11 @@ public: } void - issues_path_negative_issue() + issues_path_negative_issue(bool const domainEnabled) { - testcase("path negative: Issue #5"); + testcase( + std::string("path negative: Issue #5") + + (domainEnabled ? " w/ " : " w/o ") + "domain"); using namespace jtx; Env env = pathTestEnv(); env.fund(XRP(10000), "alice", "bob", "carol", "dan"); @@ -597,14 +728,35 @@ public: env(pay("bob", "carol", Account("bob")["USD"](75))); env.require(balance("bob", Account("carol")["USD"](-75))); env.require(balance("carol", Account("bob")["USD"](75))); + env.close(); - auto result = - find_paths(env, "alice", "bob", Account("bob")["USD"](25)); + std::optional domainID; + if (domainEnabled) + { + domainID = setupDomain(env, {"alice", "bob", "carol", "dan"}); + } + + auto result = find_paths( + env, + "alice", + "bob", + Account("bob")["USD"](25), + std::nullopt, + std::nullopt, + domainID); BEAST_EXPECT(std::get<0>(result).empty()); env(pay("alice", "bob", Account("alice")["USD"](25)), ter(tecPATH_DRY)); + env.close(); - result = find_paths(env, "alice", "bob", Account("alice")["USD"](25)); + result = find_paths( + env, + "alice", + "bob", + Account("alice")["USD"](25), + std::nullopt, + std::nullopt, + domainID); BEAST_EXPECT(std::get<0>(result).empty()); env.require(balance("alice", Account("bob")["USD"](0))); @@ -671,9 +823,11 @@ public: // bob will hold gateway AUD // alice pays bob gateway AUD using XRP void - via_offers_via_gateway() + via_offers_via_gateway(bool const domainEnabled) { - testcase("via gateway"); + testcase( + std::string("via gateway") + (domainEnabled ? " w/ " : " w/o ") + + "domain"); using namespace jtx; Env env = pathTestEnv(); auto const gw = Account("gateway"); @@ -681,15 +835,43 @@ public: env.fund(XRP(10000), "alice", "bob", "carol", gw); env.close(); env(rate(gw, 1.1)); + env.close(); env.trust(AUD(100), "bob", "carol"); + env.close(); env(pay(gw, "carol", AUD(50))); - env(offer("carol", XRP(50), AUD(50))); - env(pay("alice", "bob", AUD(10)), sendmax(XRP(100)), paths(XRP)); + env.close(); + + std::optional domainID; + if (domainEnabled) + { + domainID = setupDomain(env, {"alice", "bob", "carol", gw}); + env(offer("carol", XRP(50), AUD(50)), domain(*domainID)); + env.close(); + env(pay("alice", "bob", AUD(10)), + sendmax(XRP(100)), + paths(XRP), + domain(*domainID)); + env.close(); + } + else + { + env(offer("carol", XRP(50), AUD(50))); + env.close(); + env(pay("alice", "bob", AUD(10)), sendmax(XRP(100)), paths(XRP)); + env.close(); + } + env.require(balance("bob", AUD(10))); env.require(balance("carol", AUD(39))); - auto const result = - find_paths(env, "alice", "bob", Account("bob")["USD"](25)); + auto const result = find_paths( + env, + "alice", + "bob", + Account("bob")["USD"](25), + std::nullopt, + std::nullopt, + domainID); BEAST_EXPECT(std::get<0>(result).empty()); } @@ -865,9 +1047,11 @@ public: } void - path_find_01() + path_find_01(bool const domainEnabled) { - testcase("Path Find: XRP -> XRP and XRP -> IOU"); + testcase( + std::string("Path Find: XRP -> XRP and XRP -> IOU") + + (domainEnabled ? " w/ " : " w/o ") + "domain"); using namespace jtx; Env env = pathTestEnv(); Account A1{"A1"}; @@ -899,16 +1083,28 @@ public: env(pay(G3, M1, G3["ABC"](25000))); env.close(); - env(offer(M1, G1["XYZ"](1000), G2["XYZ"](1000))); - env(offer(M1, XRP(10000), G3["ABC"](1000))); + std::optional domainID; + if (domainEnabled) + { + domainID = setupDomain(env, {A1, A2, A3, G1, G2, G3, M1}); + env(offer(M1, G1["XYZ"](1000), G2["XYZ"](1000)), domain(*domainID)); + env(offer(M1, XRP(10000), G3["ABC"](1000)), domain(*domainID)); + env.close(); + } + else + { + env(offer(M1, G1["XYZ"](1000), G2["XYZ"](1000))); + env(offer(M1, XRP(10000), G3["ABC"](1000))); + env.close(); + } STPathSet st; STAmount sa, da; { auto const& send_amt = XRP(10); - std::tie(st, sa, da) = - find_paths(env, A1, A2, send_amt, std::nullopt, xrpCurrency()); + std::tie(st, sa, da) = find_paths( + env, A1, A2, send_amt, std::nullopt, xrpCurrency(), domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(st.empty()); } @@ -918,15 +1114,21 @@ public: // does not exist. auto const& send_amt = XRP(200); std::tie(st, sa, da) = find_paths( - env, A1, Account{"A0"}, send_amt, std::nullopt, xrpCurrency()); + env, + A1, + Account{"A0"}, + send_amt, + std::nullopt, + xrpCurrency(), + domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(st.empty()); } { auto const& send_amt = G3["ABC"](10); - std::tie(st, sa, da) = - find_paths(env, A2, G3, send_amt, std::nullopt, xrpCurrency()); + std::tie(st, sa, da) = find_paths( + env, A2, G3, send_amt, std::nullopt, xrpCurrency(), domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, XRP(100))); BEAST_EXPECT(same(st, stpath(IPE(G3["ABC"])))); @@ -934,8 +1136,8 @@ public: { auto const& send_amt = A2["ABC"](1); - std::tie(st, sa, da) = - find_paths(env, A1, A2, send_amt, std::nullopt, xrpCurrency()); + std::tie(st, sa, da) = find_paths( + env, A1, A2, send_amt, std::nullopt, xrpCurrency(), domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, XRP(10))); BEAST_EXPECT(same(st, stpath(IPE(G3["ABC"]), G3))); @@ -943,8 +1145,8 @@ public: { auto const& send_amt = A3["ABC"](1); - std::tie(st, sa, da) = - find_paths(env, A1, A3, send_amt, std::nullopt, xrpCurrency()); + std::tie(st, sa, da) = find_paths( + env, A1, A3, send_amt, std::nullopt, xrpCurrency(), domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, XRP(10))); BEAST_EXPECT(same(st, stpath(IPE(G3["ABC"]), G3, A2))); @@ -952,9 +1154,11 @@ public: } void - path_find_02() + path_find_02(bool const domainEnabled) { - testcase("Path Find: non-XRP -> XRP"); + testcase( + std::string("Path Find: non-XRP -> XRP") + + (domainEnabled ? " w/ " : " w/o ") + "domain"); using namespace jtx; Env env = pathTestEnv(); Account A1{"A1"}; @@ -975,23 +1179,53 @@ public: env(pay(G3, M1, G3["ABC"](1200))); env.close(); - env(offer(M1, G3["ABC"](1000), XRP(10000))); + std::optional domainID; + if (domainEnabled) + { + domainID = setupDomain(env, {A1, A2, G3, M1}); + env(offer(M1, G3["ABC"](1000), XRP(10000)), domain(*domainID)); + } + else + { + env(offer(M1, G3["ABC"](1000), XRP(10000))); + } STPathSet st; STAmount sa, da; - auto const& send_amt = XRP(10); - std::tie(st, sa, da) = - find_paths(env, A1, A2, send_amt, std::nullopt, A2["ABC"].currency); - BEAST_EXPECT(equal(da, send_amt)); - BEAST_EXPECT(equal(sa, A1["ABC"](1))); - BEAST_EXPECT(same(st, stpath(G3, IPE(xrpIssue())))); + + { + std::tie(st, sa, da) = find_paths( + env, + A1, + A2, + send_amt, + std::nullopt, + A2["ABC"].currency, + domainID); + BEAST_EXPECT(equal(da, send_amt)); + BEAST_EXPECT(equal(sa, A1["ABC"](1))); + BEAST_EXPECT(same(st, stpath(G3, IPE(xrpIssue())))); + } + + // domain offer will not be considered in pathfinding for non-domain + // paths + if (domainEnabled) + { + std::tie(st, sa, da) = find_paths( + env, A1, A2, send_amt, std::nullopt, A2["ABC"].currency); + BEAST_EXPECT(equal(da, send_amt)); + BEAST_EXPECT(st.empty()); + } } void - path_find_04() + path_find_04(bool const domainEnabled) { - testcase("Path Find: Bitstamp and SnapSwap, liquidity with no offers"); + testcase( + std::string( + "Path Find: Bitstamp and SnapSwap, liquidity with no offers") + + (domainEnabled ? " w/ " : " w/o ") + "domain"); using namespace jtx; Env env = pathTestEnv(); Account A1{"A1"}; @@ -1019,13 +1253,23 @@ public: env(pay(G2SW, M1, G2SW["HKD"](5000))); env.close(); + std::optional domainID; + if (domainEnabled) + domainID = setupDomain(env, {A1, A2, G1BS, G2SW, M1}); + STPathSet st; STAmount sa, da; { auto const& send_amt = A2["HKD"](10); std::tie(st, sa, da) = find_paths( - env, A1, A2, send_amt, std::nullopt, A2["HKD"].currency); + env, + A1, + A2, + send_amt, + std::nullopt, + A2["HKD"].currency, + domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, A1["HKD"](10))); BEAST_EXPECT(same(st, stpath(G1BS, M1, G2SW))); @@ -1034,7 +1278,13 @@ public: { auto const& send_amt = A1["HKD"](10); std::tie(st, sa, da) = find_paths( - env, A2, A1, send_amt, std::nullopt, A1["HKD"].currency); + env, + A2, + A1, + send_amt, + std::nullopt, + A1["HKD"].currency, + domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, A2["HKD"](10))); BEAST_EXPECT(same(st, stpath(G2SW, M1, G1BS))); @@ -1043,7 +1293,13 @@ public: { auto const& send_amt = A2["HKD"](10); std::tie(st, sa, da) = find_paths( - env, G1BS, A2, send_amt, std::nullopt, A1["HKD"].currency); + env, + G1BS, + A2, + send_amt, + std::nullopt, + A1["HKD"].currency, + domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, G1BS["HKD"](10))); BEAST_EXPECT(same(st, stpath(M1, G2SW))); @@ -1052,7 +1308,13 @@ public: { auto const& send_amt = M1["HKD"](10); std::tie(st, sa, da) = find_paths( - env, M1, G1BS, send_amt, std::nullopt, A1["HKD"].currency); + env, + M1, + G1BS, + send_amt, + std::nullopt, + A1["HKD"].currency, + domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, M1["HKD"](10))); BEAST_EXPECT(st.empty()); @@ -1061,7 +1323,13 @@ public: { auto const& send_amt = A1["HKD"](10); std::tie(st, sa, da) = find_paths( - env, G2SW, A1, send_amt, std::nullopt, A1["HKD"].currency); + env, + G2SW, + A1, + send_amt, + std::nullopt, + A1["HKD"].currency, + domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, G2SW["HKD"](10))); BEAST_EXPECT(same(st, stpath(M1, G1BS))); @@ -1069,9 +1337,11 @@ public: } void - path_find_05() + path_find_05(bool const domainEnabled) { - testcase("Path Find: non-XRP -> non-XRP, same currency"); + testcase( + std::string("Path Find: non-XRP -> non-XRP, same currency") + + (domainEnabled ? " w/ " : " w/o ") + "domain"); using namespace jtx; Env env = pathTestEnv(); Account A1{"A1"}; @@ -1108,9 +1378,21 @@ public: env(pay(G2, M2, G2["HKD"](5000))); env.close(); - env(offer(M1, G1["HKD"](1000), G2["HKD"](1000))); - env(offer(M2, XRP(10000), G2["HKD"](1000))); - env(offer(M2, G1["HKD"](1000), XRP(10000))); + std::optional domainID; + if (domainEnabled) + { + domainID = + setupDomain(env, {A1, A2, A3, A4, G1, G2, G3, G4, M1, M2}); + env(offer(M1, G1["HKD"](1000), G2["HKD"](1000)), domain(*domainID)); + env(offer(M2, XRP(10000), G2["HKD"](1000)), domain(*domainID)); + env(offer(M2, G1["HKD"](1000), XRP(10000)), domain(*domainID)); + } + else + { + env(offer(M1, G1["HKD"](1000), G2["HKD"](1000))); + env(offer(M2, XRP(10000), G2["HKD"](1000))); + env(offer(M2, G1["HKD"](1000), XRP(10000))); + } STPathSet st; STAmount sa, da; @@ -1120,7 +1402,13 @@ public: // Source -> Destination (repay source issuer) auto const& send_amt = G1["HKD"](10); std::tie(st, sa, da) = find_paths( - env, A1, G1, send_amt, std::nullopt, G1["HKD"].currency); + env, + A1, + G1, + send_amt, + std::nullopt, + G1["HKD"].currency, + domainID); BEAST_EXPECT(st.empty()); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, A1["HKD"](10))); @@ -1131,7 +1419,13 @@ public: // Source -> Destination (repay destination issuer) auto const& send_amt = A1["HKD"](10); std::tie(st, sa, da) = find_paths( - env, A1, G1, send_amt, std::nullopt, G1["HKD"].currency); + env, + A1, + G1, + send_amt, + std::nullopt, + G1["HKD"].currency, + domainID); BEAST_EXPECT(st.empty()); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, A1["HKD"](10))); @@ -1142,7 +1436,13 @@ public: // Source -> AC -> Destination auto const& send_amt = A3["HKD"](10); std::tie(st, sa, da) = find_paths( - env, A1, A3, send_amt, std::nullopt, G1["HKD"].currency); + env, + A1, + A3, + send_amt, + std::nullopt, + G1["HKD"].currency, + domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, A1["HKD"](10))); BEAST_EXPECT(same(st, stpath(G1))); @@ -1153,7 +1453,13 @@ public: // Source -> OB -> Destination auto const& send_amt = G2["HKD"](10); std::tie(st, sa, da) = find_paths( - env, G1, G2, send_amt, std::nullopt, G1["HKD"].currency); + env, + G1, + G2, + send_amt, + std::nullopt, + G1["HKD"].currency, + domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, G1["HKD"](10))); BEAST_EXPECT(same( @@ -1169,7 +1475,13 @@ public: // Source -> AC -> OB -> Destination auto const& send_amt = G2["HKD"](10); std::tie(st, sa, da) = find_paths( - env, A1, G2, send_amt, std::nullopt, G1["HKD"].currency); + env, + A1, + G2, + send_amt, + std::nullopt, + G1["HKD"].currency, + domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, A1["HKD"](10))); BEAST_EXPECT(same( @@ -1182,10 +1494,17 @@ public: { // I4) XRP bridge" -- - // Source -> AC -> OB to XRP -> OB from XRP -> AC -> Destination + // Source -> AC -> OB to XRP -> OB from XRP -> AC -> + // Destination auto const& send_amt = A2["HKD"](10); std::tie(st, sa, da) = find_paths( - env, A1, A2, send_amt, std::nullopt, G1["HKD"].currency); + env, + A1, + A2, + send_amt, + std::nullopt, + G1["HKD"].currency, + domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, A1["HKD"](10))); BEAST_EXPECT(same( @@ -1198,9 +1517,11 @@ public: } void - path_find_06() + path_find_06(bool const domainEnabled) { - testcase("Path Find: non-XRP -> non-XRP, same currency)"); + testcase( + std::string("Path Find: non-XRP -> non-XRP, same currency)") + + (domainEnabled ? " w/ " : " w/o ") + "domain"); using namespace jtx; Env env = pathTestEnv(); Account A1{"A1"}; @@ -1227,24 +1548,36 @@ public: env(pay(G2, M1, G2["HKD"](5000))); env.close(); - env(offer(M1, G1["HKD"](1000), G2["HKD"](1000))); + std::optional domainID; + if (domainEnabled) + { + domainID = setupDomain(env, {A1, A2, A3, G1, G2, M1}); + env(offer(M1, G1["HKD"](1000), G2["HKD"](1000)), domain(*domainID)); + } + else + { + env(offer(M1, G1["HKD"](1000), G2["HKD"](1000))); + } // E) Gateway to user // Source -> OB -> AC -> Destination auto const& send_amt = A2["HKD"](10); STPathSet st; STAmount sa, da; - std::tie(st, sa, da) = - find_paths(env, G1, A2, send_amt, std::nullopt, G1["HKD"].currency); + std::tie(st, sa, da) = find_paths( + env, G1, A2, send_amt, std::nullopt, G1["HKD"].currency, domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, G1["HKD"](10))); BEAST_EXPECT(same(st, stpath(M1, G2), stpath(IPE(G2["HKD"]), G2))); } void - receive_max() + receive_max(bool const domainEnabled) { - testcase("Receive max"); + testcase( + std::string("Receive max") + (domainEnabled ? " w/ " : " w/o ") + + "domain"); + using namespace jtx; auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -1260,10 +1593,28 @@ public: env.close(); env(pay(gw, charlie, USD(10))); env.close(); - env(offer(charlie, XRP(10), USD(10))); - env.close(); - auto [st, sa, da] = - find_paths(env, alice, bob, USD(-1), XRP(100).value()); + + std::optional domainID; + if (domainEnabled) + { + domainID = setupDomain(env, {alice, bob, charlie, gw}); + env(offer(charlie, XRP(10), USD(10)), domain(*domainID)); + env.close(); + } + else + { + env(offer(charlie, XRP(10), USD(10))); + env.close(); + } + + auto [st, sa, da] = find_paths( + env, + alice, + bob, + USD(-1), + XRP(100).value(), + std::nullopt, + domainID); BEAST_EXPECT(sa == XRP(10)); BEAST_EXPECT(equal(da, USD(10))); if (BEAST_EXPECT(st.size() == 1 && st[0].size() == 1)) @@ -1283,10 +1634,28 @@ public: env.close(); env(pay(gw, alice, USD(10))); env.close(); - env(offer(charlie, USD(10), XRP(10))); - env.close(); - auto [st, sa, da] = - find_paths(env, alice, bob, drops(-1), USD(100).value()); + + std::optional domainID; + if (domainEnabled) + { + domainID = setupDomain(env, {alice, bob, charlie, gw}); + env(offer(charlie, USD(10), XRP(10)), domain(*domainID)); + env.close(); + } + else + { + env(offer(charlie, USD(10), XRP(10))); + env.close(); + } + + auto [st, sa, da] = find_paths( + env, + alice, + bob, + drops(-1), + USD(100).value(), + std::nullopt, + domainID); BEAST_EXPECT(sa == USD(10)); BEAST_EXPECT(equal(da, XRP(10))); if (BEAST_EXPECT(st.size() == 1 && st[0].size() == 1)) @@ -1363,6 +1732,360 @@ public: test("no ripple -> no ripple", false, false, false); } + void + hybrid_offer_path() + { + testcase("Hybrid offer path"); + using namespace jtx; + + // test cases copied from path_find_05 and ensures path results for + // different combinations of open/domain/hybrid offers. `func` is a + // lambda param that creates different types of offers + auto testPathfind = [&](auto func, bool const domainEnabled = false) { + Env env = pathTestEnv(); + Account A1{"A1"}; + Account A2{"A2"}; + Account A3{"A3"}; + Account A4{"A4"}; + Account G1{"G1"}; + Account G2{"G2"}; + Account G3{"G3"}; + Account G4{"G4"}; + Account M1{"M1"}; + Account M2{"M2"}; + + env.fund(XRP(1000), A1, A2, A3, G1, G2, G3, G4); + env.fund(XRP(10000), A4); + env.fund(XRP(11000), M1, M2); + env.close(); + + env.trust(G1["HKD"](2000), A1); + env.trust(G2["HKD"](2000), A2); + env.trust(G1["HKD"](2000), A3); + env.trust(G1["HKD"](100000), M1); + env.trust(G2["HKD"](100000), M1); + env.trust(G1["HKD"](100000), M2); + env.trust(G2["HKD"](100000), M2); + env.close(); + + env(pay(G1, A1, G1["HKD"](1000))); + env(pay(G2, A2, G2["HKD"](1000))); + env(pay(G1, A3, G1["HKD"](1000))); + env(pay(G1, M1, G1["HKD"](1200))); + env(pay(G2, M1, G2["HKD"](5000))); + env(pay(G1, M2, G1["HKD"](1200))); + env(pay(G2, M2, G2["HKD"](5000))); + env.close(); + + std::optional domainID = + setupDomain(env, {A1, A2, A3, A4, G1, G2, G3, G4, M1, M2}); + BEAST_EXPECT(domainID); + + func(env, M1, M2, G1, G2, *domainID); + + STPathSet st; + STAmount sa, da; + + { + // A) Borrow or repay -- + // Source -> Destination (repay source issuer) + auto const& send_amt = G1["HKD"](10); + std::tie(st, sa, da) = find_paths( + env, + A1, + G1, + send_amt, + std::nullopt, + G1["HKD"].currency, + domainEnabled ? domainID : std::nullopt); + BEAST_EXPECT(st.empty()); + BEAST_EXPECT(equal(da, send_amt)); + BEAST_EXPECT(equal(sa, A1["HKD"](10))); + } + + { + // A2) Borrow or repay -- + // Source -> Destination (repay destination issuer) + auto const& send_amt = A1["HKD"](10); + std::tie(st, sa, da) = find_paths( + env, + A1, + G1, + send_amt, + std::nullopt, + G1["HKD"].currency, + domainEnabled ? domainID : std::nullopt); + BEAST_EXPECT(st.empty()); + BEAST_EXPECT(equal(da, send_amt)); + BEAST_EXPECT(equal(sa, A1["HKD"](10))); + } + + { + // B) Common gateway -- + // Source -> AC -> Destination + auto const& send_amt = A3["HKD"](10); + std::tie(st, sa, da) = find_paths( + env, + A1, + A3, + send_amt, + std::nullopt, + G1["HKD"].currency, + domainEnabled ? domainID : std::nullopt); + BEAST_EXPECT(equal(da, send_amt)); + BEAST_EXPECT(equal(sa, A1["HKD"](10))); + BEAST_EXPECT(same(st, stpath(G1))); + } + + { + // C) Gateway to gateway -- + // Source -> OB -> Destination + auto const& send_amt = G2["HKD"](10); + std::tie(st, sa, da) = find_paths( + env, + G1, + G2, + send_amt, + std::nullopt, + G1["HKD"].currency, + domainEnabled ? domainID : std::nullopt); + BEAST_EXPECT(equal(da, send_amt)); + BEAST_EXPECT(equal(sa, G1["HKD"](10))); + BEAST_EXPECT(same( + st, + stpath(IPE(G2["HKD"])), + stpath(M1), + stpath(M2), + stpath(IPE(xrpIssue()), IPE(G2["HKD"])))); + } + + { + // D) User to unlinked gateway via order book -- + // Source -> AC -> OB -> Destination + auto const& send_amt = G2["HKD"](10); + std::tie(st, sa, da) = find_paths( + env, + A1, + G2, + send_amt, + std::nullopt, + G1["HKD"].currency, + domainEnabled ? domainID : std::nullopt); + BEAST_EXPECT(equal(da, send_amt)); + BEAST_EXPECT(equal(sa, A1["HKD"](10))); + BEAST_EXPECT(same( + st, + stpath(G1, M1), + stpath(G1, M2), + stpath(G1, IPE(G2["HKD"])), + stpath(G1, IPE(xrpIssue()), IPE(G2["HKD"])))); + } + + { + // I4) XRP bridge" -- + // Source -> AC -> OB to XRP -> OB from XRP -> AC -> + // Destination + auto const& send_amt = A2["HKD"](10); + std::tie(st, sa, da) = find_paths( + env, + A1, + A2, + send_amt, + std::nullopt, + G1["HKD"].currency, + domainEnabled ? domainID : std::nullopt); + BEAST_EXPECT(equal(da, send_amt)); + BEAST_EXPECT(equal(sa, A1["HKD"](10))); + BEAST_EXPECT(same( + st, + stpath(G1, M1, G2), + stpath(G1, M2, G2), + stpath(G1, IPE(G2["HKD"]), G2), + stpath(G1, IPE(xrpIssue()), IPE(G2["HKD"]), G2))); + } + }; + + // the following tests exercise different combinations of open/hybrid + // offers to make sure that hybrid offers work in pathfinding for open + // order book + { + testPathfind([](Env& env, + Account M1, + Account M2, + Account G1, + Account G2, + uint256 domainID) { + env(offer(M1, G1["HKD"](1000), G2["HKD"](1000)), + domain(domainID), + txflags(tfHybrid)); + env(offer(M2, XRP(10000), G2["HKD"](1000))); + env(offer(M2, G1["HKD"](1000), XRP(10000))); + }); + + testPathfind([](Env& env, + Account M1, + Account M2, + Account G1, + Account G2, + uint256 domainID) { + env(offer(M1, G1["HKD"](1000), G2["HKD"](1000)), + domain(domainID), + txflags(tfHybrid)); + env(offer(M2, XRP(10000), G2["HKD"](1000)), + domain(domainID), + txflags(tfHybrid)); + env(offer(M2, G1["HKD"](1000), XRP(10000))); + }); + + testPathfind([](Env& env, + Account M1, + Account M2, + Account G1, + Account G2, + uint256 domainID) { + env(offer(M1, G1["HKD"](1000), G2["HKD"](1000)), + domain(domainID), + txflags(tfHybrid)); + env(offer(M2, XRP(10000), G2["HKD"](1000)), + domain(domainID), + txflags(tfHybrid)); + env(offer(M2, G1["HKD"](1000), XRP(10000)), + domain(domainID), + txflags(tfHybrid)); + }); + + testPathfind([](Env& env, + Account M1, + Account M2, + Account G1, + Account G2, + uint256 domainID) { + env(offer(M1, G1["HKD"](1000), G2["HKD"](1000))); + env(offer(M2, XRP(10000), G2["HKD"](1000))); + env(offer(M2, G1["HKD"](1000), XRP(10000)), + domain(domainID), + txflags(tfHybrid)); + }); + + testPathfind([](Env& env, + Account M1, + Account M2, + Account G1, + Account G2, + uint256 domainID) { + env(offer(M1, G1["HKD"](1000), G2["HKD"](1000))); + env(offer(M2, XRP(10000), G2["HKD"](1000)), + domain(domainID), + txflags(tfHybrid)); + env(offer(M2, G1["HKD"](1000), XRP(10000)), + domain(domainID), + txflags(tfHybrid)); + }); + } + + // the following tests exercise different combinations of domain/hybrid + // offers to make sure that hybrid offers work in pathfinding for domain + // order book + { + testPathfind( + [](Env& env, + Account M1, + Account M2, + Account G1, + Account G2, + uint256 domainID) { + env(offer(M1, G1["HKD"](1000), G2["HKD"](1000)), + domain(domainID), + txflags(tfHybrid)); + env(offer(M2, XRP(10000), G2["HKD"](1000)), + domain(domainID)); + env(offer(M2, G1["HKD"](1000), XRP(10000)), + domain(domainID)); + }, + true); + + testPathfind( + [](Env& env, + Account M1, + Account M2, + Account G1, + Account G2, + uint256 domainID) { + env(offer(M1, G1["HKD"](1000), G2["HKD"](1000)), + domain(domainID), + txflags(tfHybrid)); + env(offer(M2, XRP(10000), G2["HKD"](1000)), + domain(domainID), + txflags(tfHybrid)); + env(offer(M2, G1["HKD"](1000), XRP(10000)), + domain(domainID)); + }, + true); + + testPathfind( + [](Env& env, + Account M1, + Account M2, + Account G1, + Account G2, + uint256 domainID) { + env(offer(M1, G1["HKD"](1000), G2["HKD"](1000)), + domain(domainID)); + env(offer(M2, XRP(10000), G2["HKD"](1000)), + domain(domainID)); + env(offer(M2, G1["HKD"](1000), XRP(10000)), + domain(domainID), + txflags(tfHybrid)); + }, + true); + + testPathfind( + [](Env& env, + Account M1, + Account M2, + Account G1, + Account G2, + uint256 domainID) { + env(offer(M1, G1["HKD"](1000), G2["HKD"](1000)), + domain(domainID)); + env(offer(M2, XRP(10000), G2["HKD"](1000)), + domain(domainID), + txflags(tfHybrid)); + env(offer(M2, G1["HKD"](1000), XRP(10000)), + domain(domainID), + txflags(tfHybrid)); + }, + true); + } + } + + void + amm_domain_path() + { + testcase("AMM not used in domain path"); + using namespace jtx; + Env env = pathTestEnv(); + PermissionedDEX permDex(env); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + permDex; + AMM amm(env, alice, XRP(10), USD(50)); + + STPathSet st; + STAmount sa, da; + + auto const& send_amt = XRP(1); + + // doing pathfind with domain won't include amm + std::tie(st, sa, da) = find_paths( + env, bob, carol, send_amt, std::nullopt, USD.currency, domainID); + BEAST_EXPECT(st.empty()); + + // a non-domain pathfind returns amm in the path + std::tie(st, sa, da) = + find_paths(env, bob, carol, send_amt, std::nullopt, USD.currency); + BEAST_EXPECT(same(st, stpath(gw, IPE(xrpIssue())))); + } + void run() override { @@ -1370,35 +2093,43 @@ public: no_direct_path_no_intermediary_no_alternatives(); direct_path_no_intermediary(); payment_auto_path_find(); - path_find(); - path_find_consume_all(); - alternative_path_consume_both(); - alternative_paths_consume_best_transfer(); + indirect_paths_path_find(); alternative_paths_consume_best_transfer_first(); - alternative_paths_limit_returned_paths_to_best_quality(); - issues_path_negative_issue(); issues_path_negative_ripple_client_issue_23_smaller(); issues_path_negative_ripple_client_issue_23_larger(); - via_offers_via_gateway(); - indirect_paths_path_find(); quality_paths_quality_set_and_test(); trust_auto_clear_trust_normal_clear(); trust_auto_clear_trust_auto_clear(); - xrp_to_xrp(); - receive_max(); noripple_combinations(); - // The following path_find_NN tests are data driven tests - // that were originally implemented in js/coffee and migrated - // here. The quantities and currencies used are taken directly from - // those legacy tests, which in some cases probably represented - // customer use cases. + for (bool const domainEnabled : {false, true}) + { + path_find(domainEnabled); + path_find_consume_all(domainEnabled); + alternative_path_consume_both(domainEnabled); + alternative_paths_consume_best_transfer(domainEnabled); + alternative_paths_limit_returned_paths_to_best_quality( + domainEnabled); + issues_path_negative_issue(domainEnabled); + via_offers_via_gateway(domainEnabled); + xrp_to_xrp(domainEnabled); + receive_max(domainEnabled); - path_find_01(); - path_find_02(); - path_find_04(); - path_find_05(); - path_find_06(); + // The following path_find_NN tests are data driven tests + // that were originally implemented in js/coffee and migrated + // here. The quantities and currencies used are taken directly from + // those legacy tests, which in some cases probably represented + // customer use cases. + + path_find_01(domainEnabled); + path_find_02(domainEnabled); + path_find_04(domainEnabled); + path_find_05(domainEnabled); + path_find_06(domainEnabled); + } + + hybrid_offer_path(); + amm_domain_path(); } }; diff --git a/src/test/app/PayChan_test.cpp b/src/test/app/PayChan_test.cpp index 7cb1542453..3a5d3d6ff5 100644 --- a/src/test/app/PayChan_test.cpp +++ b/src/test/app/PayChan_test.cpp @@ -1035,7 +1035,7 @@ struct PayChan_test : public beast::unit_test::suite { // Credentials amendment not enabled - Env env(*this, supported_amendments() - featureCredentials); + Env env(*this, testable_amendments() - featureCredentials); env.fund(XRP(5000), "alice", "bob"); env.close(); @@ -2344,7 +2344,7 @@ public: run() override { using namespace test::jtx; - FeatureBitset const all{supported_amendments()}; + FeatureBitset const all{testable_amendments()}; testWithFeats(all - disallowIncoming); testWithFeats(all); testDepositAuthCreds(); diff --git a/src/test/app/PayStrand_test.cpp b/src/test/app/PayStrand_test.cpp index 4d743d9d7c..936fe403d4 100644 --- a/src/test/app/PayStrand_test.cpp +++ b/src/test/app/PayStrand_test.cpp @@ -27,6 +27,9 @@ #include #include #include +#include + +#include namespace ripple { namespace test { @@ -656,6 +659,7 @@ struct PayStrand_test : public beast::unit_test::suite true, OfferCrossing::no, ammContext, + std::nullopt, env.app().logs().journal("Flow")); BEAST_EXPECT(ter == expTer); if (sizeof...(expSteps) != 0) @@ -684,6 +688,7 @@ struct PayStrand_test : public beast::unit_test::suite true, OfferCrossing::no, ammContext, + std::nullopt, env.app().logs().journal("Flow")); (void)_; BEAST_EXPECT(ter == tesSUCCESS); @@ -701,6 +706,7 @@ struct PayStrand_test : public beast::unit_test::suite true, OfferCrossing::no, ammContext, + std::nullopt, env.app().logs().journal("Flow")); (void)_; BEAST_EXPECT(ter == tesSUCCESS); @@ -738,7 +744,7 @@ struct PayStrand_test : public beast::unit_test::suite STPath(), tesSUCCESS, D{alice, gw, usdC}, - B{USD, EUR}, + B{USD, EUR, std::nullopt}, D{gw, bob, eurC}); // Path with explicit offer @@ -749,7 +755,7 @@ struct PayStrand_test : public beast::unit_test::suite STPath({ipe(EUR)}), tesSUCCESS, D{alice, gw, usdC}, - B{USD, EUR}, + B{USD, EUR, std::nullopt}, D{gw, bob, eurC}); // Path with offer that changes issuer only @@ -761,7 +767,7 @@ struct PayStrand_test : public beast::unit_test::suite STPath({iape(carol)}), tesSUCCESS, D{alice, gw, usdC}, - B{USD, carol["USD"]}, + B{USD, carol["USD"], std::nullopt}, D{carol, bob, usdC}); // Path with XRP src currency @@ -772,7 +778,7 @@ struct PayStrand_test : public beast::unit_test::suite STPath({ipe(USD)}), tesSUCCESS, XRPS{alice}, - B{XRP, USD}, + B{XRP, USD, std::nullopt}, D{gw, bob, usdC}); // Path with XRP dst currency. @@ -787,7 +793,7 @@ struct PayStrand_test : public beast::unit_test::suite xrpAccount()}}), tesSUCCESS, D{alice, gw, usdC}, - B{USD, XRP}, + B{USD, XRP, std::nullopt}, XRPS{bob}); // Path with XRP cross currency bridged payment @@ -798,8 +804,8 @@ struct PayStrand_test : public beast::unit_test::suite STPath({cpe(xrpCurrency())}), tesSUCCESS, D{alice, gw, usdC}, - B{USD, XRP}, - B{XRP, EUR}, + B{USD, XRP, std::nullopt}, + B{XRP, EUR, std::nullopt}, D{gw, bob, eurC}); // XRP -> XRP transaction can't include a path @@ -821,6 +827,7 @@ struct PayStrand_test : public beast::unit_test::suite true, OfferCrossing::no, ammContext, + std::nullopt, flowJournal); BEAST_EXPECT(r.first == temBAD_PATH); } @@ -837,6 +844,7 @@ struct PayStrand_test : public beast::unit_test::suite true, OfferCrossing::no, ammContext, + std::nullopt, flowJournal); BEAST_EXPECT(r.first == temBAD_PATH); } @@ -853,6 +861,7 @@ struct PayStrand_test : public beast::unit_test::suite true, OfferCrossing::no, ammContext, + std::nullopt, flowJournal); BEAST_EXPECT(r.first == temBAD_PATH); } @@ -990,6 +999,7 @@ struct PayStrand_test : public beast::unit_test::suite true, OfferCrossing::no, ammContext, + std::nullopt, env.app().logs().journal("Flow")); BEAST_EXPECT(ter == tesSUCCESS); BEAST_EXPECT(equal(strand, D{alice, gw, usdC})); @@ -1017,12 +1027,13 @@ struct PayStrand_test : public beast::unit_test::suite false, OfferCrossing::no, ammContext, + std::nullopt, env.app().logs().journal("Flow")); BEAST_EXPECT(ter == tesSUCCESS); BEAST_EXPECT(equal( strand, D{alice, gw, usdC}, - B{USD.issue(), xrpIssue()}, + B{USD.issue(), xrpIssue(), std::nullopt}, XRPS{bob})); } } @@ -1201,6 +1212,7 @@ struct PayStrand_test : public beast::unit_test::suite dstAcc, noAccount(), pathSet, + std::nullopt, env.app().logs(), &inputs); BEAST_EXPECT(r.result() == temBAD_PATH); @@ -1213,6 +1225,7 @@ struct PayStrand_test : public beast::unit_test::suite noAccount(), srcAcc, pathSet, + std::nullopt, env.app().logs(), &inputs); BEAST_EXPECT(r.result() == temBAD_PATH); @@ -1225,6 +1238,7 @@ struct PayStrand_test : public beast::unit_test::suite dstAcc, srcAcc, pathSet, + std::nullopt, env.app().logs(), &inputs); BEAST_EXPECT(r.result() == temBAD_PATH); @@ -1237,6 +1251,7 @@ struct PayStrand_test : public beast::unit_test::suite dstAcc, srcAcc, pathSet, + std::nullopt, env.app().logs(), &inputs); BEAST_EXPECT(r.result() == temBAD_PATH); @@ -1252,14 +1267,14 @@ struct PayStrand_test : public beast::unit_test::suite run() override { using namespace jtx; - auto const sa = supported_amendments(); - testToStrand(sa - featureFlowCross); + auto const sa = testable_amendments(); + testToStrand(sa - featurePermissionedDEX); testToStrand(sa); - testRIPD1373(sa - featureFlowCross); + testRIPD1373(sa - featurePermissionedDEX); testRIPD1373(sa); - testLoop(sa - featureFlowCross); + testLoop(sa - featurePermissionedDEX); testLoop(sa); testNoAccount(sa); diff --git a/src/test/app/PermissionedDEX_test.cpp b/src/test/app/PermissionedDEX_test.cpp new file mode 100644 index 0000000000..3fd3a35f45 --- /dev/null +++ b/src/test/app/PermissionedDEX_test.cpp @@ -0,0 +1,1577 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { +namespace test { + +using namespace jtx; + +class PermissionedDEX_test : public beast::unit_test::suite +{ + [[nodiscard]] bool + offerExists(Env const& env, Account const& account, std::uint32_t offerSeq) + { + return static_cast(env.le(keylet::offer(account.id(), offerSeq))); + } + + [[nodiscard]] bool + checkOffer( + Env const& env, + Account const& account, + std::uint32_t offerSeq, + STAmount const& takerPays, + STAmount const& takerGets, + uint32_t const flags = 0, + bool const domainOffer = false) + { + auto offerInDir = [&](uint256 const& directory, + uint64_t const pageIndex, + std::optional domain = + std::nullopt) -> bool { + auto const page = env.le(keylet::page(directory, pageIndex)); + if (!page) + return false; + + if (domain != (*page)[~sfDomainID]) + return false; + + auto const& indexes = page->getFieldV256(sfIndexes); + for (auto const& index : indexes) + { + if (index == keylet::offer(account, offerSeq).key) + return true; + } + + return false; + }; + + auto const sle = env.le(keylet::offer(account.id(), offerSeq)); + if (!sle) + return false; + if (sle->getFieldAmount(sfTakerGets) != takerGets) + return false; + if (sle->getFieldAmount(sfTakerPays) != takerPays) + return false; + if (sle->getFlags() != flags) + return false; + if (domainOffer && !sle->isFieldPresent(sfDomainID)) + return false; + if (!domainOffer && sle->isFieldPresent(sfDomainID)) + return false; + if (!offerInDir( + sle->getFieldH256(sfBookDirectory), + sle->getFieldU64(sfBookNode), + (*sle)[~sfDomainID])) + return false; + + if (sle->isFlag(lsfHybrid)) + { + if (!sle->isFieldPresent(sfDomainID)) + return false; + if (!sle->isFieldPresent(sfAdditionalBooks)) + return false; + if (sle->getFieldArray(sfAdditionalBooks).size() != 1) + return false; + + auto const& additionalBookDirs = + sle->getFieldArray(sfAdditionalBooks); + + for (auto const& bookDir : additionalBookDirs) + { + auto const& dirIndex = bookDir.getFieldH256(sfBookDirectory); + auto const& dirNode = bookDir.getFieldU64(sfBookNode); + + // the directory is for the open order book, so the dir + // doesn't have domainID + if (!offerInDir(dirIndex, dirNode, std::nullopt)) + return false; + } + } + else + { + if (sle->isFieldPresent(sfAdditionalBooks)) + return false; + } + + return true; + } + + uint256 + getBookDirKey( + Book const& book, + STAmount const& takerPays, + STAmount const& takerGets) + { + return keylet::quality( + keylet::book(book), getRate(takerGets, takerPays)) + .key; + } + + std::optional + getDefaultOfferDirKey( + Env const& env, + Account const& account, + std::uint32_t offerSeq) + { + if (auto const sle = env.le(keylet::offer(account.id(), offerSeq))) + return Keylet(ltDIR_NODE, (*sle)[sfBookDirectory]).key; + + return {}; + } + + [[nodiscard]] bool + checkDirectorySize(Env const& env, uint256 directory, std::uint32_t dirSize) + { + std::optional pageIndex{0}; + std::uint32_t dirCnt = 0; + + do + { + auto const page = env.le(keylet::page(directory, *pageIndex)); + if (!page) + break; + + pageIndex = (*page)[~sfIndexNext]; + dirCnt += (*page)[sfIndexes].size(); + + } while (pageIndex.value_or(0)); + + return dirCnt == dirSize; + } + + void + testOfferCreate(FeatureBitset features) + { + testcase("OfferCreate"); + + // test preflight + { + Env env(*this, features - featurePermissionedDEX); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + env(offer(bob, XRP(10), USD(10)), + domain(domainID), + ter(temDISABLED)); + env.close(); + + env.enableFeature(featurePermissionedDEX); + env.close(); + env(offer(bob, XRP(10), USD(10)), domain(domainID)); + env.close(); + } + + // preclaim - someone outside of the domain cannot create domain offer + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + // create devin account who is not part of the domain + Account devin("devin"); + env.fund(XRP(1000), devin); + env.close(); + env.trust(USD(1000), devin); + env.close(); + env(pay(gw, devin, USD(100))); + env.close(); + + env(offer(devin, XRP(10), USD(10)), + domain(domainID), + ter(tecNO_PERMISSION)); + env.close(); + + // domain owner also issues a credential for devin + env(credentials::create(devin, domainOwner, credType)); + env.close(); + + // devin still cannot create offer since he didn't accept credential + env(offer(devin, XRP(10), USD(10)), + domain(domainID), + ter(tecNO_PERMISSION)); + env.close(); + + env(credentials::accept(devin, domainOwner, credType)); + env.close(); + + env(offer(devin, XRP(10), USD(10)), domain(domainID)); + env.close(); + } + + // preclaim - someone with expired cred cannot create domain offer + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + // create devin account who is not part of the domain + Account devin("devin"); + env.fund(XRP(1000), devin); + env.close(); + env.trust(USD(1000), devin); + env.close(); + env(pay(gw, devin, USD(100))); + env.close(); + + auto jv = credentials::create(devin, domainOwner, credType); + uint32_t const t = env.current() + ->info() + .parentCloseTime.time_since_epoch() + .count(); + jv[sfExpiration.jsonName] = t + 20; + env(jv); + + env(credentials::accept(devin, domainOwner, credType)); + env.close(); + + // devin can still create offer while his cred is not expired + env(offer(devin, XRP(10), USD(10)), domain(domainID)); + env.close(); + + // time advance + env.close(std::chrono::seconds(20)); + + // devin cannot create offer with expired cred + env(offer(devin, XRP(10), USD(10)), + domain(domainID), + ter(tecNO_PERMISSION)); + env.close(); + } + + // preclaim - cannot create an offer in a non existent domain + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + uint256 const badDomain{ + "F10D0CC9A0F9A3CBF585B80BE09A186483668FDBDD39AA7E3370F3649CE134" + "E5"}; + + env(offer(bob, XRP(10), USD(10)), + domain(badDomain), + ter(tecNO_PERMISSION)); + env.close(); + } + + // apply - offer can be created even if takergets issuer is not in + // domain + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + env(credentials::deleteCred( + domainOwner, gw, domainOwner, credType)); + env.close(); + + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), domain(domainID)); + env.close(); + + BEAST_EXPECT( + checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true)); + } + + // apply - offer can be created even if takerpays issuer is not in + // domain + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + env(credentials::deleteCred( + domainOwner, gw, domainOwner, credType)); + env.close(); + + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, USD(10), XRP(10)), domain(domainID)); + env.close(); + + BEAST_EXPECT( + checkOffer(env, bob, bobOfferSeq, USD(10), XRP(10), 0, true)); + } + + // apply - two domain offers cross with each other + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), domain(domainID)); + env.close(); + + BEAST_EXPECT( + checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(ownerCount(env, bob) == 3); + + // a non domain offer cannot cross with domain offer + env(offer(carol, USD(10), XRP(10))); + env.close(); + + BEAST_EXPECT( + checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true)); + + auto const aliceOfferSeq{env.seq(alice)}; + env(offer(alice, USD(10), XRP(10)), domain(domainID)); + env.close(); + + BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT(ownerCount(env, alice) == 2); + } + + // apply - create lots of domain offers + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + std::vector offerSeqs; + offerSeqs.reserve(100); + + for (size_t i = 0; i <= 100; i++) + { + auto const bobOfferSeq{env.seq(bob)}; + offerSeqs.emplace_back(bobOfferSeq); + + env(offer(bob, XRP(10), USD(10)), domain(domainID)); + env.close(); + BEAST_EXPECT(checkOffer( + env, bob, bobOfferSeq, XRP(10), USD(10), 0, true)); + } + + for (auto const offerSeq : offerSeqs) + { + env(offer_cancel(bob, offerSeq)); + env.close(); + BEAST_EXPECT(!offerExists(env, bob, offerSeq)); + } + } + } + + void + testPayment(FeatureBitset features) + { + testcase("Payment"); + + // test preflight - without enabling featurePermissionedDEX amendment + { + Env env(*this, features - featurePermissionedDEX); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + env(pay(bob, alice, USD(10)), + path(~USD), + sendmax(XRP(10)), + domain(domainID), + ter(temDISABLED)); + env.close(); + + env.enableFeature(featurePermissionedDEX); + env.close(); + + env(offer(bob, XRP(10), USD(10)), domain(domainID)); + env.close(); + + env(pay(bob, alice, USD(10)), + path(~USD), + sendmax(XRP(10)), + domain(domainID)); + env.close(); + } + + // preclaim - cannot send payment with non existent domain + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + uint256 const badDomain{ + "F10D0CC9A0F9A3CBF585B80BE09A186483668FDBDD39AA7E3370F3649CE134" + "E5"}; + + env(pay(bob, alice, USD(10)), + path(~USD), + sendmax(XRP(10)), + domain(badDomain), + ter(tecNO_PERMISSION)); + env.close(); + } + + // preclaim - payment with non-domain destination fails + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + env(offer(bob, XRP(10), USD(10)), domain(domainID)); + env.close(); + + // create devin account who is not part of the domain + Account devin("devin"); + env.fund(XRP(1000), devin); + env.close(); + env.trust(USD(1000), devin); + env.close(); + env(pay(gw, devin, USD(100))); + env.close(); + + // devin is not part of domain + env(pay(alice, devin, USD(10)), + path(~USD), + sendmax(XRP(10)), + domain(domainID), + ter(tecNO_PERMISSION)); + env.close(); + + // domain owner also issues a credential for devin + env(credentials::create(devin, domainOwner, credType)); + env.close(); + + // devin has not yet accepted cred + env(pay(alice, devin, USD(10)), + path(~USD), + sendmax(XRP(10)), + domain(domainID), + ter(tecNO_PERMISSION)); + env.close(); + + env(credentials::accept(devin, domainOwner, credType)); + env.close(); + + // devin can now receive payment after he is in domain + env(pay(alice, devin, USD(10)), + path(~USD), + sendmax(XRP(10)), + domain(domainID)); + env.close(); + } + + // preclaim - non-domain sender cannot send payment + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + env(offer(bob, XRP(10), USD(10)), domain(domainID)); + env.close(); + + // create devin account who is not part of the domain + Account devin("devin"); + env.fund(XRP(1000), devin); + env.close(); + env.trust(USD(1000), devin); + env.close(); + env(pay(gw, devin, USD(100))); + env.close(); + + // devin tries to send domain payment + env(pay(devin, alice, USD(10)), + path(~USD), + sendmax(XRP(10)), + domain(domainID), + ter(tecNO_PERMISSION)); + env.close(); + + // domain owner also issues a credential for devin + env(credentials::create(devin, domainOwner, credType)); + env.close(); + + // devin has not yet accepted cred + env(pay(devin, alice, USD(10)), + path(~USD), + sendmax(XRP(10)), + domain(domainID), + ter(tecNO_PERMISSION)); + env.close(); + + env(credentials::accept(devin, domainOwner, credType)); + env.close(); + + // devin can now send payment after he is in domain + env(pay(devin, alice, USD(10)), + path(~USD), + sendmax(XRP(10)), + domain(domainID)); + env.close(); + } + + // apply - domain owner can always send and receive domain payment + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + env(offer(bob, XRP(10), USD(10)), domain(domainID)); + env.close(); + + // domain owner can always be destination + env(pay(alice, domainOwner, USD(10)), + path(~USD), + sendmax(XRP(10)), + domain(domainID)); + env.close(); + + env(offer(bob, XRP(10), USD(10)), domain(domainID)); + env.close(); + + // domain owner can send + env(pay(domainOwner, alice, USD(10)), + path(~USD), + sendmax(XRP(10)), + domain(domainID)); + env.close(); + } + } + + void + testBookStep(FeatureBitset features) + { + testcase("Book step"); + + // test domain cross currency payment consuming one offer + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + // create a regular offer without domain + auto const regularOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10))); + env.close(); + BEAST_EXPECT( + checkOffer(env, bob, regularOfferSeq, XRP(10), USD(10))); + + auto const regularDirKey = + getDefaultOfferDirKey(env, bob, regularOfferSeq); + BEAST_EXPECT(regularDirKey); + BEAST_EXPECT(checkDirectorySize(env, *regularDirKey, 1)); + + // a domain payment cannot consume regular offers + env(pay(alice, carol, USD(10)), + path(~USD), + sendmax(XRP(10)), + domain(domainID), + ter(tecPATH_PARTIAL)); + env.close(); + + // create a domain offer + auto const domainOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), domain(domainID)); + env.close(); + + BEAST_EXPECT(checkOffer( + env, bob, domainOfferSeq, XRP(10), USD(10), 0, true)); + + auto const domainDirKey = + getDefaultOfferDirKey(env, bob, domainOfferSeq); + BEAST_EXPECT(domainDirKey); + BEAST_EXPECT(checkDirectorySize(env, *domainDirKey, 1)); + + // cross-currency permissioned payment consumed + // domain offer instead of regular offer + env(pay(alice, carol, USD(10)), + path(~USD), + sendmax(XRP(10)), + domain(domainID)); + env.close(); + BEAST_EXPECT(!offerExists(env, bob, domainOfferSeq)); + BEAST_EXPECT( + checkOffer(env, bob, regularOfferSeq, XRP(10), USD(10))); + + // domain directory is empty + BEAST_EXPECT(checkDirectorySize(env, *domainDirKey, 0)); + BEAST_EXPECT(checkDirectorySize(env, *regularDirKey, 1)); + } + + // test domain payment consuming two offers in the path + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + auto const EUR = gw["EUR"]; + env.trust(EUR(1000), alice); + env.close(); + env.trust(EUR(1000), bob); + env.close(); + env.trust(EUR(1000), carol); + env.close(); + env(pay(gw, bob, EUR(100))); + env.close(); + + // create XRP/USD domain offer + auto const usdOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), domain(domainID)); + env.close(); + + BEAST_EXPECT( + checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, true)); + + // payment fail because there isn't eur offer + env(pay(alice, carol, EUR(10)), + path(~USD, ~EUR), + sendmax(XRP(10)), + domain(domainID), + ter(tecPATH_PARTIAL)); + env.close(); + BEAST_EXPECT( + checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, true)); + + // bob creates a regular USD/EUR offer + auto const regularOfferSeq{env.seq(bob)}; + env(offer(bob, USD(10), EUR(10))); + env.close(); + BEAST_EXPECT( + checkOffer(env, bob, regularOfferSeq, USD(10), EUR(10))); + + // alice tries to pay again, but still fails because the regular + // offer cannot be consumed + env(pay(alice, carol, EUR(10)), + path(~USD, ~EUR), + sendmax(XRP(10)), + domain(domainID), + ter(tecPATH_PARTIAL)); + env.close(); + + // bob creates a domain USD/EUR offer + auto const eurOfferSeq{env.seq(bob)}; + env(offer(bob, USD(10), EUR(10)), domain(domainID)); + env.close(); + BEAST_EXPECT( + checkOffer(env, bob, eurOfferSeq, USD(10), EUR(10), 0, true)); + + // alice successfully consume two domain offers: xrp/usd and usd/eur + env(pay(alice, carol, EUR(5)), + sendmax(XRP(5)), + domain(domainID), + path(~USD, ~EUR)); + env.close(); + + BEAST_EXPECT( + checkOffer(env, bob, usdOfferSeq, XRP(5), USD(5), 0, true)); + BEAST_EXPECT( + checkOffer(env, bob, eurOfferSeq, USD(5), EUR(5), 0, true)); + + // alice successfully consume two domain offers and deletes them + // we compute path this time using `paths` + env(pay(alice, carol, EUR(5)), + sendmax(XRP(5)), + domain(domainID), + paths(XRP)); + env.close(); + + BEAST_EXPECT(!offerExists(env, bob, usdOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, eurOfferSeq)); + + // regular offer is not consumed + BEAST_EXPECT( + checkOffer(env, bob, regularOfferSeq, USD(10), EUR(10))); + } + + // domain payment cannot consume offer from another domain + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + // Fund devin and create USD trustline + Account badDomainOwner("badDomainOwner"); + Account devin("devin"); + env.fund(XRP(1000), badDomainOwner, devin); + env.close(); + env.trust(USD(1000), devin); + env.close(); + env(pay(gw, devin, USD(100))); + env.close(); + + auto const badCredType = "badCred"; + pdomain::Credentials credentials{{badDomainOwner, badCredType}}; + env(pdomain::setTx(badDomainOwner, credentials)); + + auto objects = pdomain::getObjects(badDomainOwner, env); + auto const badDomainID = objects.begin()->first; + + env(credentials::create(devin, badDomainOwner, badCredType)); + env.close(); + env(credentials::accept(devin, badDomainOwner, badCredType)); + + // devin creates a domain offer in another domain + env(offer(devin, XRP(10), USD(10)), domain(badDomainID)); + env.close(); + + // domain payment can't consume an offer from another domain + env(pay(alice, carol, USD(10)), + path(~USD), + sendmax(XRP(10)), + domain(domainID), + ter(tecPATH_PARTIAL)); + env.close(); + + // bob creates an offer under the right domain + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), domain(domainID)); + env.close(); + BEAST_EXPECT( + checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true)); + + // domain payment now consumes from the right domain + env(pay(alice, carol, USD(10)), + path(~USD), + sendmax(XRP(10)), + domain(domainID)); + env.close(); + + BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq)); + } + + // sanity check: devin, who is part of the domain but doesn't have a + // trustline with USD issuer, can successfully make a payment using + // offer + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + env(offer(bob, XRP(10), USD(10)), domain(domainID)); + env.close(); + + // fund devin but don't create a USD trustline with gateway + Account devin("devin"); + env.fund(XRP(1000), devin); + env.close(); + + // domain owner also issues a credential for devin + env(credentials::create(devin, domainOwner, credType)); + env.close(); + + env(credentials::accept(devin, domainOwner, credType)); + env.close(); + + // successful payment because offer is consumed + env(pay(devin, alice, USD(10)), sendmax(XRP(10)), domain(domainID)); + env.close(); + } + + // offer becomes unfunded when offer owner's cred expires + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + // create devin account who is not part of the domain + Account devin("devin"); + env.fund(XRP(1000), devin); + env.close(); + env.trust(USD(1000), devin); + env.close(); + env(pay(gw, devin, USD(100))); + env.close(); + + auto jv = credentials::create(devin, domainOwner, credType); + uint32_t const t = env.current() + ->info() + .parentCloseTime.time_since_epoch() + .count(); + jv[sfExpiration.jsonName] = t + 20; + env(jv); + + env(credentials::accept(devin, domainOwner, credType)); + env.close(); + + // devin can still create offer while his cred is not expired + auto const offerSeq{env.seq(devin)}; + env(offer(devin, XRP(10), USD(10)), domain(domainID)); + env.close(); + + // devin's offer can still be consumed while his cred isn't expired + env(pay(alice, carol, USD(5)), + path(~USD), + sendmax(XRP(5)), + domain(domainID)); + env.close(); + BEAST_EXPECT( + checkOffer(env, devin, offerSeq, XRP(5), USD(5), 0, true)); + + // advance time + env.close(std::chrono::seconds(20)); + + // devin's offer is unfunded now due to expired cred + env(pay(alice, carol, USD(5)), + path(~USD), + sendmax(XRP(5)), + domain(domainID), + ter(tecPATH_PARTIAL)); + env.close(); + BEAST_EXPECT( + checkOffer(env, devin, offerSeq, XRP(5), USD(5), 0, true)); + } + + // offer becomes unfunded when offer owner's cred is removed + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + auto const offerSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), domain(domainID)); + env.close(); + + // bob's offer can still be consumed while his cred exists + env(pay(alice, carol, USD(5)), + path(~USD), + sendmax(XRP(5)), + domain(domainID)); + env.close(); + BEAST_EXPECT( + checkOffer(env, bob, offerSeq, XRP(5), USD(5), 0, true)); + + // remove bob's cred + env(credentials::deleteCred( + domainOwner, bob, domainOwner, credType)); + env.close(); + + // bob's offer is unfunded now due to expired cred + env(pay(alice, carol, USD(5)), + path(~USD), + sendmax(XRP(5)), + domain(domainID), + ter(tecPATH_PARTIAL)); + env.close(); + BEAST_EXPECT( + checkOffer(env, bob, offerSeq, XRP(5), USD(5), 0, true)); + } + } + + void + testRippling(FeatureBitset features) + { + testcase("Rippling"); + + // test a non-domain account can still be part of rippling in a domain + // payment. If the domain wishes to control who is allowed to ripple + // through, they should set the rippling individually + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + auto const EURA = alice["EUR"]; + auto const EURB = bob["EUR"]; + + env.trust(EURA(100), bob); + env.trust(EURB(100), carol); + env.close(); + + // remove bob from domain + env(credentials::deleteCred(domainOwner, bob, domainOwner, credType)); + env.close(); + + // alice can still ripple through bob even though he's not part + // of the domain, this is intentional + env(pay(alice, carol, EURB(10)), paths(EURA), domain(domainID)); + env.close(); + env.require(balance(bob, EURA(10)), balance(carol, EURB(10))); + + // carol sets no ripple on bob + env(trust(carol, bob["EUR"](0), bob, tfSetNoRipple)); + env.close(); + + // payment no longer works because carol has no ripple on bob + env(pay(alice, carol, EURB(5)), + paths(EURA), + domain(domainID), + ter(tecPATH_DRY)); + env.close(); + env.require(balance(bob, EURA(10)), balance(carol, EURB(10))); + } + + void + testOfferTokenIssuerInDomain(FeatureBitset features) + { + testcase("Offer token issuer in domain"); + + // whether the issuer is in the domain should NOT affect whether an + // offer can be consumed in domain payment + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + // create an xrp/usd offer with usd as takergets + auto const bobOffer1Seq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), domain(domainID)); + env.close(); + + // create an usd/xrp offer with usd as takerpays + auto const bobOffer2Seq{env.seq(bob)}; + env(offer(bob, USD(10), XRP(10)), domain(domainID), txflags(tfPassive)); + env.close(); + + BEAST_EXPECT( + checkOffer(env, bob, bobOffer1Seq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(checkOffer( + env, bob, bobOffer2Seq, USD(10), XRP(10), lsfPassive, true)); + + // remove gateway from domain + env(credentials::deleteCred(domainOwner, gw, domainOwner, credType)); + env.close(); + + // payment succeeds even if issuer is not in domain + // xrp/usd offer is consumed + env(pay(alice, carol, USD(10)), + path(~USD), + sendmax(XRP(10)), + domain(domainID)); + env.close(); + BEAST_EXPECT(!offerExists(env, bob, bobOffer1Seq)); + + // payment succeeds even if issuer is not in domain + // usd/xrp offer is consumed + env(pay(alice, carol, XRP(10)), + path(~XRP), + sendmax(USD(10)), + domain(domainID)); + env.close(); + BEAST_EXPECT(!offerExists(env, bob, bobOffer2Seq)); + } + + void + testRemoveUnfundedOffer(FeatureBitset features) + { + testcase("Remove unfunded offer"); + + // checking that an unfunded offer will be implictly removed by a + // successfuly payment tx + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + auto const aliceOfferSeq{env.seq(alice)}; + env(offer(alice, XRP(100), USD(100)), domain(domainID)); + env.close(); + + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(20), USD(20)), domain(domainID)); + env.close(); + + BEAST_EXPECT( + checkOffer(env, bob, bobOfferSeq, XRP(20), USD(20), 0, true)); + BEAST_EXPECT( + checkOffer(env, alice, aliceOfferSeq, XRP(100), USD(100), 0, true)); + + auto const domainDirKey = getDefaultOfferDirKey(env, bob, bobOfferSeq); + BEAST_EXPECT(domainDirKey); + BEAST_EXPECT(checkDirectorySize(env, *domainDirKey, 2)); + + // remove alice from domain and thus alice's offer becomes unfunded + env(credentials::deleteCred(domainOwner, alice, domainOwner, credType)); + env.close(); + + env(pay(gw, carol, USD(10)), + path(~USD), + sendmax(XRP(10)), + domain(domainID)); + env.close(); + + BEAST_EXPECT( + checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true)); + + // alice's unfunded offer is removed implicitly + BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq)); + BEAST_EXPECT(checkDirectorySize(env, *domainDirKey, 1)); + } + + void + testAmmNotUsed(FeatureBitset features) + { + testcase("AMM not used"); + + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + AMM amm(env, alice, XRP(10), USD(50)); + + // a domain payment isn't able to consume AMM + env(pay(bob, carol, USD(5)), + path(~USD), + sendmax(XRP(5)), + domain(domainID), + ter(tecPATH_PARTIAL)); + env.close(); + + // a non domain payment can use AMM + env(pay(bob, carol, USD(5)), path(~USD), sendmax(XRP(5))); + env.close(); + + // USD amount in AMM is changed + auto [xrp, usd, lpt] = amm.balances(XRP, USD); + BEAST_EXPECT(usd == USD(45)); + } + + void + testHybridOfferCreate(FeatureBitset features) + { + testcase("Hybrid offer create"); + + // test preflight - invalid hybrid flag + { + Env env(*this, features - featurePermissionedDEX); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + env(offer(bob, XRP(10), USD(10)), + domain(domainID), + txflags(tfHybrid), + ter(temDISABLED)); + env.close(); + + env(offer(bob, XRP(10), USD(10)), + txflags(tfHybrid), + ter(temINVALID_FLAG)); + env.close(); + + env.enableFeature(featurePermissionedDEX); + env.close(); + + // hybrid offer must have domainID + env(offer(bob, XRP(10), USD(10)), + txflags(tfHybrid), + ter(temINVALID_FLAG)); + env.close(); + + // hybrid offer must have domainID + auto const offerSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), + txflags(tfHybrid), + domain(domainID)); + env.close(); + BEAST_EXPECT(checkOffer( + env, bob, offerSeq, XRP(10), USD(10), lsfHybrid, true)); + } + + // apply - domain offer can cross with hybrid + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), + txflags(tfHybrid), + domain(domainID)); + env.close(); + + BEAST_EXPECT(checkOffer( + env, bob, bobOfferSeq, XRP(10), USD(10), lsfHybrid, true)); + BEAST_EXPECT(offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT(ownerCount(env, bob) == 3); + + auto const aliceOfferSeq{env.seq(alice)}; + env(offer(alice, USD(10), XRP(10)), domain(domainID)); + env.close(); + + BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT(ownerCount(env, alice) == 2); + } + + // apply - open offer can cross with hybrid + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), + txflags(tfHybrid), + domain(domainID)); + env.close(); + + BEAST_EXPECT(offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT(ownerCount(env, bob) == 3); + BEAST_EXPECT(checkOffer( + env, bob, bobOfferSeq, XRP(10), USD(10), lsfHybrid, true)); + + auto const aliceOfferSeq{env.seq(alice)}; + env(offer(alice, USD(10), XRP(10))); + env.close(); + + BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT(ownerCount(env, alice) == 2); + } + + // apply - by default, hybrid offer tries to cross with offers in the + // domain book + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), domain(domainID)); + env.close(); + + BEAST_EXPECT( + checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(ownerCount(env, bob) == 3); + + // hybrid offer auto crosses with domain offer + auto const aliceOfferSeq{env.seq(alice)}; + env(offer(alice, USD(10), XRP(10)), + domain(domainID), + txflags(tfHybrid)); + env.close(); + + BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT(ownerCount(env, alice) == 2); + } + + // apply - hybrid offer does not automatically cross with open offers + // because by default, it only tries to cross domain offers + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10))); + env.close(); + + BEAST_EXPECT( + checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, false)); + BEAST_EXPECT(ownerCount(env, bob) == 3); + + // hybrid offer auto crosses with domain offer + auto const aliceOfferSeq{env.seq(alice)}; + env(offer(alice, USD(10), XRP(10)), + domain(domainID), + txflags(tfHybrid)); + env.close(); + + BEAST_EXPECT(offerExists(env, alice, aliceOfferSeq)); + BEAST_EXPECT(offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT( + checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, false)); + BEAST_EXPECT(checkOffer( + env, alice, aliceOfferSeq, USD(10), XRP(10), lsfHybrid, true)); + BEAST_EXPECT(ownerCount(env, alice) == 3); + } + } + + void + testHybridInvalidOffer(FeatureBitset features) + { + testcase("Hybrid invalid offer"); + + // bob has a hybrid offer and then he is removed from domain. + // in this case, the hybrid offer will be considered as unfunded even in + // a regular payment + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + auto const hybridOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(50), USD(50)), txflags(tfHybrid), domain(domainID)); + env.close(); + + // remove bob from domain + env(credentials::deleteCred(domainOwner, bob, domainOwner, credType)); + env.close(); + + // bob's hybrid offer is unfunded and can not be consumed in a domain + // payment + env(pay(alice, carol, USD(5)), + path(~USD), + sendmax(XRP(5)), + domain(domainID), + ter(tecPATH_PARTIAL)); + env.close(); + BEAST_EXPECT(checkOffer( + env, bob, hybridOfferSeq, XRP(50), USD(50), lsfHybrid, true)); + + // bob's unfunded hybrid offer can't be consumed even with a regular + // payment + env(pay(alice, carol, USD(5)), + path(~USD), + sendmax(XRP(5)), + ter(tecPATH_PARTIAL)); + env.close(); + BEAST_EXPECT(checkOffer( + env, bob, hybridOfferSeq, XRP(50), USD(50), lsfHybrid, true)); + + // create a regular offer + auto const regularOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10))); + env.close(); + BEAST_EXPECT(offerExists(env, bob, regularOfferSeq)); + BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, XRP(10), USD(10))); + + auto const sleHybridOffer = + env.le(keylet::offer(bob.id(), hybridOfferSeq)); + BEAST_EXPECT(sleHybridOffer); + auto const openDir = + sleHybridOffer->getFieldArray(sfAdditionalBooks)[0].getFieldH256( + sfBookDirectory); + BEAST_EXPECT(checkDirectorySize(env, openDir, 2)); + + // this normal payment should consume the regular offer and remove the + // unfunded hybrid offer + env(pay(alice, carol, USD(5)), path(~USD), sendmax(XRP(5))); + env.close(); + + BEAST_EXPECT(!offerExists(env, bob, hybridOfferSeq)); + BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, XRP(5), USD(5))); + BEAST_EXPECT(checkDirectorySize(env, openDir, 1)); + } + + void + testHybridBookStep(FeatureBitset features) + { + testcase("Hybrid book step"); + + // both non domain and domain payments can consume hybrid offer + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + auto const hybridOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), + txflags(tfHybrid), + domain(domainID)); + env.close(); + + env(pay(alice, carol, USD(5)), + path(~USD), + sendmax(XRP(5)), + domain(domainID)); + env.close(); + BEAST_EXPECT(checkOffer( + env, bob, hybridOfferSeq, XRP(5), USD(5), lsfHybrid, true)); + + // hybrid offer can't be consumed since bob is not in domain anymore + env(pay(alice, carol, USD(5)), path(~USD), sendmax(XRP(5))); + env.close(); + + BEAST_EXPECT(!offerExists(env, bob, hybridOfferSeq)); + } + + // someone from another domain can't cross hybrid if they specified + // wrong domainID + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + // Fund accounts + Account badDomainOwner("badDomainOwner"); + Account devin("devin"); + env.fund(XRP(1000), badDomainOwner, devin); + env.close(); + + auto const badCredType = "badCred"; + pdomain::Credentials credentials{{badDomainOwner, badCredType}}; + env(pdomain::setTx(badDomainOwner, credentials)); + + auto objects = pdomain::getObjects(badDomainOwner, env); + auto const badDomainID = objects.begin()->first; + + env(credentials::create(devin, badDomainOwner, badCredType)); + env.close(); + env(credentials::accept(devin, badDomainOwner, badCredType)); + env.close(); + + auto const hybridOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), + txflags(tfHybrid), + domain(domainID)); + env.close(); + + // other domains can't consume the offer + env(pay(devin, badDomainOwner, USD(5)), + path(~USD), + sendmax(XRP(5)), + domain(badDomainID), + ter(tecPATH_DRY)); + env.close(); + BEAST_EXPECT(checkOffer( + env, bob, hybridOfferSeq, XRP(10), USD(10), lsfHybrid, true)); + + env(pay(alice, carol, USD(5)), + path(~USD), + sendmax(XRP(5)), + domain(domainID)); + env.close(); + BEAST_EXPECT(checkOffer( + env, bob, hybridOfferSeq, XRP(5), USD(5), lsfHybrid, true)); + + // hybrid offer can't be consumed since bob is not in domain anymore + env(pay(alice, carol, USD(5)), path(~USD), sendmax(XRP(5))); + env.close(); + + BEAST_EXPECT(!offerExists(env, bob, hybridOfferSeq)); + } + + // test domain payment consuming two offers w/ hybrid offer + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + auto const EUR = gw["EUR"]; + env.trust(EUR(1000), alice); + env.close(); + env.trust(EUR(1000), bob); + env.close(); + env.trust(EUR(1000), carol); + env.close(); + env(pay(gw, bob, EUR(100))); + env.close(); + + auto const usdOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), domain(domainID)); + env.close(); + + BEAST_EXPECT( + checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, true)); + + // payment fail because there isn't eur offer + env(pay(alice, carol, EUR(5)), + path(~USD, ~EUR), + sendmax(XRP(5)), + domain(domainID), + ter(tecPATH_PARTIAL)); + env.close(); + BEAST_EXPECT( + checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, true)); + + // bob creates a hybrid eur offer + auto const eurOfferSeq{env.seq(bob)}; + env(offer(bob, USD(10), EUR(10)), + domain(domainID), + txflags(tfHybrid)); + env.close(); + BEAST_EXPECT(checkOffer( + env, bob, eurOfferSeq, USD(10), EUR(10), lsfHybrid, true)); + + // alice successfully consume two domain offers: xrp/usd and usd/eur + env(pay(alice, carol, EUR(5)), + path(~USD, ~EUR), + sendmax(XRP(5)), + domain(domainID)); + env.close(); + + BEAST_EXPECT( + checkOffer(env, bob, usdOfferSeq, XRP(5), USD(5), 0, true)); + BEAST_EXPECT(checkOffer( + env, bob, eurOfferSeq, USD(5), EUR(5), lsfHybrid, true)); + } + + // test regular payment using a regular offer and a hybrid offer + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + auto const EUR = gw["EUR"]; + env.trust(EUR(1000), alice); + env.close(); + env.trust(EUR(1000), bob); + env.close(); + env.trust(EUR(1000), carol); + env.close(); + env(pay(gw, bob, EUR(100))); + env.close(); + + // bob creates a regular usd offer + auto const usdOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10))); + env.close(); + + BEAST_EXPECT( + checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, false)); + + // bob creates a hybrid eur offer + auto const eurOfferSeq{env.seq(bob)}; + env(offer(bob, USD(10), EUR(10)), + domain(domainID), + txflags(tfHybrid)); + env.close(); + BEAST_EXPECT(checkOffer( + env, bob, eurOfferSeq, USD(10), EUR(10), lsfHybrid, true)); + + // alice successfully consume two offers: xrp/usd and usd/eur + env(pay(alice, carol, EUR(5)), path(~USD, ~EUR), sendmax(XRP(5))); + env.close(); + + BEAST_EXPECT( + checkOffer(env, bob, usdOfferSeq, XRP(5), USD(5), 0, false)); + BEAST_EXPECT(checkOffer( + env, bob, eurOfferSeq, USD(5), EUR(5), lsfHybrid, true)); + } + } + + void + testHybridOfferDirectories(FeatureBitset features) + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + std::vector offerSeqs; + offerSeqs.reserve(100); + + Book domainBook{Issue(XRP), Issue(USD), domainID}; + Book openBook{Issue(XRP), Issue(USD), std::nullopt}; + + auto const domainDir = getBookDirKey(domainBook, XRP(10), USD(10)); + auto const openDir = getBookDirKey(openBook, XRP(10), USD(10)); + + size_t dirCnt = 100; + + for (size_t i = 1; i <= dirCnt; i++) + { + auto const bobOfferSeq{env.seq(bob)}; + offerSeqs.emplace_back(bobOfferSeq); + env(offer(bob, XRP(10), USD(10)), + txflags(tfHybrid), + domain(domainID)); + env.close(); + + auto const sleOffer = env.le(keylet::offer(bob.id(), bobOfferSeq)); + BEAST_EXPECT(sleOffer); + BEAST_EXPECT(sleOffer->getFieldH256(sfBookDirectory) == domainDir); + BEAST_EXPECT( + sleOffer->getFieldArray(sfAdditionalBooks).size() == 1); + BEAST_EXPECT( + sleOffer->getFieldArray(sfAdditionalBooks)[0].getFieldH256( + sfBookDirectory) == openDir); + + BEAST_EXPECT(checkOffer( + env, bob, bobOfferSeq, XRP(10), USD(10), lsfHybrid, true)); + BEAST_EXPECT(checkDirectorySize(env, domainDir, i)); + BEAST_EXPECT(checkDirectorySize(env, openDir, i)); + } + + for (auto const offerSeq : offerSeqs) + { + env(offer_cancel(bob, offerSeq)); + env.close(); + dirCnt--; + BEAST_EXPECT(!offerExists(env, bob, offerSeq)); + BEAST_EXPECT(checkDirectorySize(env, domainDir, dirCnt)); + BEAST_EXPECT(checkDirectorySize(env, openDir, dirCnt)); + } + } + + void + testAutoBridge(FeatureBitset features) + { + testcase("Auto bridge"); + + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + auto const EUR = gw["EUR"]; + + for (auto const& account : {alice, bob, carol}) + { + env(trust(account, EUR(10000))); + env.close(); + } + + env(pay(gw, carol, EUR(1))); + env.close(); + + auto const aliceOfferSeq{env.seq(alice)}; + auto const bobOfferSeq{env.seq(bob)}; + env(offer(alice, XRP(100), USD(1)), domain(domainID)); + env(offer(bob, EUR(1), XRP(100)), domain(domainID)); + env.close(); + + // carol's offer should cross bob and alice's offers due to auto + // bridging + auto const carolOfferSeq{env.seq(carol)}; + env(offer(carol, USD(1), EUR(1)), domain(domainID)); + env.close(); + + BEAST_EXPECT(!offerExists(env, bob, aliceOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, carolOfferSeq)); + } + +public: + void + run() override + { + FeatureBitset const all{jtx::testable_amendments()}; + + // Test domain offer (w/o hyrbid) + testOfferCreate(all); + testPayment(all); + testBookStep(all); + testRippling(all); + testOfferTokenIssuerInDomain(all); + testRemoveUnfundedOffer(all); + testAmmNotUsed(all); + testAutoBridge(all); + + // Test hybrid offers + testHybridOfferCreate(all); + testHybridBookStep(all); + testHybridInvalidOffer(all); + testHybridOfferDirectories(all); + } +}; + +BEAST_DEFINE_TESTSUITE(PermissionedDEX, app, ripple); + +} // namespace test +} // namespace ripple diff --git a/src/test/app/PermissionedDomains_test.cpp b/src/test/app/PermissionedDomains_test.cpp index e33a88fa08..31e34ccf17 100644 --- a/src/test/app/PermissionedDomains_test.cpp +++ b/src/test/app/PermissionedDomains_test.cpp @@ -53,9 +53,9 @@ exceptionExpected(Env& env, Json::Value const& jv) class PermissionedDomains_test : public beast::unit_test::suite { FeatureBitset withoutFeature_{ - supported_amendments() - featurePermissionedDomains}; + testable_amendments() - featurePermissionedDomains}; FeatureBitset withFeature_{ - supported_amendments() // + testable_amendments() // | featurePermissionedDomains | featureCredentials}; // Verify that each tx type can execute if the feature is enabled. @@ -81,7 +81,7 @@ class PermissionedDomains_test : public beast::unit_test::suite void testCredentialsDisabled() { - auto amendments = supported_amendments(); + auto amendments = testable_amendments(); amendments.set(featurePermissionedDomains); amendments.reset(featureCredentials); testcase("Credentials disabled"); diff --git a/src/test/app/PseudoTx_test.cpp b/src/test/app/PseudoTx_test.cpp index d96828a50b..53adf795c2 100644 --- a/src/test/app/PseudoTx_test.cpp +++ b/src/test/app/PseudoTx_test.cpp @@ -115,7 +115,7 @@ struct PseudoTx_test : public beast::unit_test::suite run() override { using namespace test::jtx; - FeatureBitset const all{supported_amendments()}; + FeatureBitset const all{testable_amendments()}; FeatureBitset const xrpFees{featureXRPFees}; testPrevented(all - featureXRPFees); diff --git a/src/test/app/ReducedOffer_test.cpp b/src/test/app/ReducedOffer_test.cpp index 546a07d93e..5142aaab0e 100644 --- a/src/test/app/ReducedOffer_test.cpp +++ b/src/test/app/ReducedOffer_test.cpp @@ -82,8 +82,8 @@ public: // Make one test run without fixReducedOffersV1 and one with. for (FeatureBitset features : - {supported_amendments() - fixReducedOffersV1, - supported_amendments() | fixReducedOffersV1}) + {testable_amendments() - fixReducedOffersV1, + testable_amendments() | fixReducedOffersV1}) { Env env{*this, features}; @@ -238,8 +238,8 @@ public: // Make one test run without fixReducedOffersV1 and one with. for (FeatureBitset features : - {supported_amendments() - fixReducedOffersV1, - supported_amendments() | fixReducedOffersV1}) + {testable_amendments() - fixReducedOffersV1, + testable_amendments() | fixReducedOffersV1}) { // Make sure none of the offers we generate are under funded. Env env{*this, features}; @@ -401,8 +401,8 @@ public: // Make one test run without fixReducedOffersV1 and one with. for (FeatureBitset features : - {supported_amendments() - fixReducedOffersV1, - supported_amendments() | fixReducedOffersV1}) + {testable_amendments() - fixReducedOffersV1, + testable_amendments() | fixReducedOffersV1}) { Env env{*this, features}; @@ -509,8 +509,8 @@ public: // Make one test run without fixReducedOffersV1 and one with. for (FeatureBitset features : - {supported_amendments() - fixReducedOffersV1, - supported_amendments() | fixReducedOffersV1}) + {testable_amendments() - fixReducedOffersV1, + testable_amendments() | fixReducedOffersV1}) { Env env{*this, features}; @@ -639,8 +639,8 @@ public: // Make one test run without fixReducedOffersV2 and one with. for (FeatureBitset features : - {supported_amendments() - fixReducedOffersV2, - supported_amendments() | fixReducedOffersV2}) + {testable_amendments() - fixReducedOffersV2, + testable_amendments() | fixReducedOffersV2}) { // Make sure none of the offers we generate are under funded. Env env{*this, features}; diff --git a/src/test/app/SetAuth_test.cpp b/src/test/app/SetAuth_test.cpp index e55fbc4d5d..4c63560770 100644 --- a/src/test/app/SetAuth_test.cpp +++ b/src/test/app/SetAuth_test.cpp @@ -74,8 +74,8 @@ struct SetAuth_test : public beast::unit_test::suite run() override { using namespace jtx; - auto const sa = supported_amendments(); - testAuth(sa - featureFlowCross); + auto const sa = testable_amendments(); + testAuth(sa - featurePermissionedDEX); testAuth(sa); } }; diff --git a/src/test/app/SetRegularKey_test.cpp b/src/test/app/SetRegularKey_test.cpp index 6a3a5ff2a9..78b75fc458 100644 --- a/src/test/app/SetRegularKey_test.cpp +++ b/src/test/app/SetRegularKey_test.cpp @@ -32,7 +32,7 @@ public: using namespace test::jtx; testcase("Set regular key"); - Env env{*this, supported_amendments() - fixMasterKeyAsRegularKey}; + Env env{*this, testable_amendments() - fixMasterKeyAsRegularKey}; Account const alice("alice"); Account const bob("bob"); env.fund(XRP(10000), alice, bob); @@ -72,7 +72,7 @@ public: using namespace test::jtx; testcase("Set regular key"); - Env env{*this, supported_amendments() | fixMasterKeyAsRegularKey}; + Env env{*this, testable_amendments() | fixMasterKeyAsRegularKey}; Account const alice("alice"); Account const bob("bob"); env.fund(XRP(10000), alice, bob); @@ -109,7 +109,7 @@ public: // See https://ripplelabs.atlassian.net/browse/RIPD-1721. testcase( "Set regular key to master key (before fixMasterKeyAsRegularKey)"); - Env env{*this, supported_amendments() - fixMasterKeyAsRegularKey}; + Env env{*this, testable_amendments() - fixMasterKeyAsRegularKey}; Account const alice("alice"); env.fund(XRP(10000), alice); @@ -139,7 +139,7 @@ public: testcase( "Set regular key to master key (after fixMasterKeyAsRegularKey)"); - Env env{*this, supported_amendments() | fixMasterKeyAsRegularKey}; + Env env{*this, testable_amendments() | fixMasterKeyAsRegularKey}; Account const alice("alice"); env.fund(XRP(10000), alice); diff --git a/src/test/app/SetTrust_test.cpp b/src/test/app/SetTrust_test.cpp index 9b4048bf9c..18457b5faf 100644 --- a/src/test/app/SetTrust_test.cpp +++ b/src/test/app/SetTrust_test.cpp @@ -648,7 +648,7 @@ public: run() override { using namespace test::jtx; - auto const sa = supported_amendments(); + auto const sa = testable_amendments(); testWithFeats(sa - disallowIncoming); testWithFeats(sa); } diff --git a/src/test/app/TheoreticalQuality_test.cpp b/src/test/app/TheoreticalQuality_test.cpp index 0269d206cc..a8713ec69a 100644 --- a/src/test/app/TheoreticalQuality_test.cpp +++ b/src/test/app/TheoreticalQuality_test.cpp @@ -264,9 +264,10 @@ class TheoreticalQuality_test : public beast::unit_test::suite sendMaxIssue, rcp.paths, /*defaultPaths*/ rcp.paths.empty(), - sb.rules().enabled(featureOwnerPaysFee), + false, OfferCrossing::no, ammContext, + std::nullopt, dummyJ); BEAST_EXPECT(sr.first == tesSUCCESS); @@ -358,7 +359,7 @@ public: // Tests are sped up by a factor of 2 if a new environment isn't created // on every iteration. - Env env(*this, supported_amendments()); + Env env(*this, testable_amendments()); for (int i = 0; i < numTestIterations; ++i) { auto const iterAsStr = std::to_string(i); @@ -433,7 +434,7 @@ public: // Speed up tests by creating the environment outside the loop // (factor of 2 speedup on the DirectStep tests) - Env env(*this, supported_amendments()); + Env env(*this, testable_amendments()); for (int i = 0; i < numTestIterations; ++i) { auto const iterAsStr = std::to_string(i); diff --git a/src/test/app/Ticket_test.cpp b/src/test/app/Ticket_test.cpp index dd83e3036e..f8ac64679e 100644 --- a/src/test/app/Ticket_test.cpp +++ b/src/test/app/Ticket_test.cpp @@ -385,7 +385,7 @@ class Ticket_test : public beast::unit_test::suite testcase("Feature Not Enabled"); using namespace test::jtx; - Env env{*this, supported_amendments() - featureTicketBatch}; + Env env{*this, testable_amendments() - featureTicketBatch}; env(ticket::create(env.master, 1), ter(temDISABLED)); env.close(); @@ -933,7 +933,7 @@ class Ticket_test : public beast::unit_test::suite // Try the test without featureTicketBatch enabled. using namespace test::jtx; { - Env env{*this, supported_amendments() - featureTicketBatch}; + Env env{*this, testable_amendments() - featureTicketBatch}; Account alice{"alice"}; env.fund(XRP(10000), alice); @@ -957,7 +957,7 @@ class Ticket_test : public beast::unit_test::suite } // Try the test with featureTicketBatch enabled. { - Env env{*this, supported_amendments()}; + Env env{*this, testable_amendments()}; Account alice{"alice"}; env.fund(XRP(10000), alice); diff --git a/src/test/app/TrustAndBalance_test.cpp b/src/test/app/TrustAndBalance_test.cpp index 037a7e0d89..f39d9e0313 100644 --- a/src/test/app/TrustAndBalance_test.cpp +++ b/src/test/app/TrustAndBalance_test.cpp @@ -480,8 +480,8 @@ public: }; using namespace test::jtx; - auto const sa = supported_amendments(); - testWithFeatures(sa - featureFlowCross); + auto const sa = testable_amendments(); + testWithFeatures(sa - featurePermissionedDEX); testWithFeatures(sa); } }; diff --git a/src/test/app/TxQ_test.cpp b/src/test/app/TxQ_test.cpp index 7b69cee1ce..d0965cc8ff 100644 --- a/src/test/app/TxQ_test.cpp +++ b/src/test/app/TxQ_test.cpp @@ -18,6 +18,7 @@ //============================================================================== #include +#include #include #include #include @@ -42,97 +43,6 @@ class TxQPosNegFlows_test : public beast::unit_test::suite static constexpr FeeLevel64 baseFeeLevel{256}; static constexpr FeeLevel64 minEscalationFeeLevel = baseFeeLevel * 500; - void - checkMetrics( - int line, - jtx::Env& env, - std::size_t expectedCount, - std::optional expectedMaxCount, - std::size_t expectedInLedger, - std::size_t expectedPerLedger, - std::uint64_t expectedMinFeeLevel = baseFeeLevel.fee(), - std::uint64_t expectedMedFeeLevel = minEscalationFeeLevel.fee()) - { - FeeLevel64 const expectedMin{expectedMinFeeLevel}; - FeeLevel64 const expectedMed{expectedMedFeeLevel}; - auto const metrics = env.app().getTxQ().getMetrics(*env.current()); - using namespace std::string_literals; - - metrics.referenceFeeLevel == baseFeeLevel - ? pass() - : fail( - "reference: "s + - std::to_string(metrics.referenceFeeLevel.value()) + "/" + - std::to_string(baseFeeLevel.value()), - __FILE__, - line); - - metrics.txCount == expectedCount - ? pass() - : fail( - "txCount: "s + std::to_string(metrics.txCount) + "/" + - std::to_string(expectedCount), - __FILE__, - line); - - metrics.txQMaxSize == expectedMaxCount - ? pass() - : fail( - "txQMaxSize: "s + - std::to_string(metrics.txQMaxSize.value_or(0)) + "/" + - std::to_string(expectedMaxCount.value_or(0)), - __FILE__, - line); - - metrics.txInLedger == expectedInLedger - ? pass() - : fail( - "txInLedger: "s + std::to_string(metrics.txInLedger) + "/" + - std::to_string(expectedInLedger), - __FILE__, - line); - - metrics.txPerLedger == expectedPerLedger - ? pass() - : fail( - "txPerLedger: "s + std::to_string(metrics.txPerLedger) + "/" + - std::to_string(expectedPerLedger), - __FILE__, - line); - - metrics.minProcessingFeeLevel == expectedMin - ? pass() - : fail( - "minProcessingFeeLevel: "s + - std::to_string(metrics.minProcessingFeeLevel.value()) + - "/" + std::to_string(expectedMin.value()), - __FILE__, - line); - - metrics.medFeeLevel == expectedMed - ? pass() - : fail( - "medFeeLevel: "s + - std::to_string(metrics.medFeeLevel.value()) + "/" + - std::to_string(expectedMed.value()), - __FILE__, - line); - - auto const expectedCurFeeLevel = expectedInLedger > expectedPerLedger - ? expectedMed * expectedInLedger * expectedInLedger / - (expectedPerLedger * expectedPerLedger) - : metrics.referenceFeeLevel; - - metrics.openLedgerFeeLevel == expectedCurFeeLevel - ? pass() - : fail( - "openLedgerFeeLevel: "s + - std::to_string(metrics.openLedgerFeeLevel.value()) + "/" + - std::to_string(expectedCurFeeLevel.value()), - __FILE__, - line); - } - void fillQueue(jtx::Env& env, jtx::Account const& account) { @@ -189,40 +99,6 @@ class TxQPosNegFlows_test : public beast::unit_test::suite return calcMedFeeLevel(feeLevel, feeLevel); } - static std::unique_ptr - makeConfig( - std::map extraTxQ = {}, - std::map extraVoting = {}) - { - auto p = test::jtx::envconfig(); - auto& section = p->section("transaction_queue"); - section.set("ledgers_in_queue", "2"); - section.set("minimum_queue_size", "2"); - section.set("min_ledgers_to_compute_size_limit", "3"); - section.set("max_ledger_counts_to_store", "100"); - section.set("retry_sequence_percent", "25"); - section.set("normal_consensus_increase_percent", "0"); - - for (auto const& [k, v] : extraTxQ) - section.set(k, v); - - // Some tests specify different fee settings that are enabled by - // a FeeVote - if (!extraVoting.empty()) - { - auto& votingSection = p->section("voting"); - for (auto const& [k, v] : extraVoting) - { - votingSection.set(k, v); - } - - // In order for the vote to occur, we must run as a validator - p->section("validation_seed") - .legacy("shUwVw52ofnCUX5m7kPTKzJdr4HEH"); - } - return p; - } - std::size_t initFee( jtx::Env& env, @@ -244,7 +120,7 @@ class TxQPosNegFlows_test : public beast::unit_test::suite // transactions as though they are ordinary transactions. auto const flagPerLedger = 1 + ripple::detail::numUpVotedAmendments(); auto const flagMaxQueue = ledgersInQueue * flagPerLedger; - checkMetrics(__LINE__, env, 0, flagMaxQueue, 0, flagPerLedger); + checkMetrics(*this, env, 0, flagMaxQueue, 0, flagPerLedger); // Pad a couple of txs with normal fees so the median comes // back down to normal @@ -255,7 +131,7 @@ class TxQPosNegFlows_test : public beast::unit_test::suite // metrics to reset to defaults, EXCEPT the maxQueue size. using namespace std::chrono_literals; env.close(env.now() + 5s, 10000ms); - checkMetrics(__LINE__, env, 0, flagMaxQueue, 0, expectedPerLedger); + checkMetrics(*this, env, 0, flagMaxQueue, 0, expectedPerLedger); auto const fees = env.current()->fees(); BEAST_EXPECT(fees.base == XRPAmount{base}); BEAST_EXPECT(fees.reserve == XRPAmount{reserve}); @@ -287,37 +163,37 @@ public: auto queued = ter(terQUEUED); auto const baseFee = env.current()->fees().base.drops(); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 3); + checkMetrics(*this, env, 0, std::nullopt, 0, 3); // Create several accounts while the fee is cheap so they all apply. env.fund(XRP(50000), noripple(alice, bob, charlie, daria)); - checkMetrics(__LINE__, env, 0, std::nullopt, 4, 3); + checkMetrics(*this, env, 0, std::nullopt, 4, 3); // Alice - price starts exploding: held env(noop(alice), queued); - checkMetrics(__LINE__, env, 1, std::nullopt, 4, 3); + checkMetrics(*this, env, 1, std::nullopt, 4, 3); // Bob with really high fee - applies env(noop(bob), fee(openLedgerCost(env))); - checkMetrics(__LINE__, env, 1, std::nullopt, 5, 3); + checkMetrics(*this, env, 1, std::nullopt, 5, 3); // Daria with low fee: hold env(noop(daria), fee(baseFee * 100), queued); - checkMetrics(__LINE__, env, 2, std::nullopt, 5, 3); + checkMetrics(*this, env, 2, std::nullopt, 5, 3); env.close(); // Verify that the held transactions got applied - checkMetrics(__LINE__, env, 0, 10, 2, 5); + checkMetrics(*this, env, 0, 10, 2, 5); ////////////////////////////////////////////////////////////// // Make some more accounts. We'll need them later to abuse the queue. env.fund(XRP(50000), noripple(elmo, fred, gwen, hank)); - checkMetrics(__LINE__, env, 0, 10, 6, 5); + checkMetrics(*this, env, 0, 10, 6, 5); // Now get a bunch of transactions held. env(noop(alice), fee(baseFee * 1.2), queued); - checkMetrics(__LINE__, env, 1, 10, 6, 5); + checkMetrics(*this, env, 1, 10, 6, 5); env(noop(bob), fee(baseFee), queued); // won't clear the queue env(noop(charlie), fee(baseFee * 2), queued); @@ -326,11 +202,11 @@ public: env(noop(fred), fee(baseFee * 1.9), queued); env(noop(gwen), fee(baseFee * 1.6), queued); env(noop(hank), fee(baseFee * 1.8), queued); - checkMetrics(__LINE__, env, 8, 10, 6, 5); + checkMetrics(*this, env, 8, 10, 6, 5); env.close(); // Verify that the held transactions got applied - checkMetrics(__LINE__, env, 1, 12, 7, 6); + checkMetrics(*this, env, 1, 12, 7, 6); // Bob's transaction is still stuck in the queue. @@ -339,45 +215,45 @@ public: // Hank sends another txn env(noop(hank), fee(baseFee), queued); // But he's not going to leave it in the queue - checkMetrics(__LINE__, env, 2, 12, 7, 6); + checkMetrics(*this, env, 2, 12, 7, 6); // Hank sees his txn got held and bumps the fee, // but doesn't even bump it enough to requeue env(noop(hank), fee(baseFee * 1.1), ter(telCAN_NOT_QUEUE_FEE)); - checkMetrics(__LINE__, env, 2, 12, 7, 6); + checkMetrics(*this, env, 2, 12, 7, 6); // Hank sees his txn got held and bumps the fee, // enough to requeue, but doesn't bump it enough to // apply to the ledger env(noop(hank), fee(baseFee * 600), queued); // But he's not going to leave it in the queue - checkMetrics(__LINE__, env, 2, 12, 7, 6); + checkMetrics(*this, env, 2, 12, 7, 6); // Hank sees his txn got held and bumps the fee, // high enough to get into the open ledger, because // he doesn't want to wait. env(noop(hank), fee(openLedgerCost(env))); - checkMetrics(__LINE__, env, 1, 12, 8, 6); + checkMetrics(*this, env, 1, 12, 8, 6); // Hank then sends another, less important txn // (In addition to the metrics, this will verify that // the original txn got removed.) env(noop(hank), fee(baseFee * 2), queued); - checkMetrics(__LINE__, env, 2, 12, 8, 6); + checkMetrics(*this, env, 2, 12, 8, 6); env.close(); // Verify that bob and hank's txns were applied - checkMetrics(__LINE__, env, 0, 16, 2, 8); + checkMetrics(*this, env, 0, 16, 2, 8); // Close again with a simulated time leap to // reset the escalation limit down to minimum env.close(env.now() + 5s, 10000ms); - checkMetrics(__LINE__, env, 0, 16, 0, 3); + checkMetrics(*this, env, 0, 16, 0, 3); // Then close once more without the time leap // to reset the queue maxsize down to minimum env.close(); - checkMetrics(__LINE__, env, 0, 6, 0, 3); + checkMetrics(*this, env, 0, 6, 0, 3); ////////////////////////////////////////////////////////////// @@ -390,7 +266,7 @@ public: env(noop(gwen), fee(largeFee)); env(noop(fred), fee(largeFee)); env(noop(elmo), fee(largeFee)); - checkMetrics(__LINE__, env, 0, 6, 4, 3); + checkMetrics(*this, env, 0, 6, 4, 3); // Use explicit fees so we can control which txn // will get dropped @@ -406,7 +282,7 @@ public: // Queue is full now. // clang-format off - checkMetrics(__LINE__, env, 6, 6, 4, 3, txFeeLevelByAccount(env, daria) + 1); + checkMetrics(*this, env, 6, 6, 4, 3, txFeeLevelByAccount(env, daria) + 1); // clang-format on // Try to add another transaction with the default (low) fee, // it should fail because the queue is full. @@ -419,7 +295,7 @@ public: // Queue is still full, of course, but the min fee has gone up // clang-format off - checkMetrics(__LINE__, env, 6, 6, 4, 3, txFeeLevelByAccount(env, elmo) + 1); + checkMetrics(*this, env, 6, 6, 4, 3, txFeeLevelByAccount(env, elmo) + 1); // clang-format on // Close out the ledger, the transactions are accepted, the @@ -428,11 +304,11 @@ public: // is put back in. Neat. env.close(); // clang-format off - checkMetrics(__LINE__, env, 2, 8, 5, 4, baseFeeLevel.fee(), calcMedFeeLevel(FeeLevel64{baseFeeLevel.fee() * largeFeeMultiplier})); + checkMetrics(*this, env, 2, 8, 5, 4, baseFeeLevel.fee(), calcMedFeeLevel(FeeLevel64{baseFeeLevel.fee() * largeFeeMultiplier})); // clang-format on env.close(); - checkMetrics(__LINE__, env, 0, 10, 2, 5); + checkMetrics(*this, env, 0, 10, 2, 5); ////////////////////////////////////////////////////////////// @@ -446,10 +322,10 @@ public: env(noop(daria)); env(pay(alice, iris, XRP(1000)), queued); env(noop(iris), seq(1), fee(baseFee * 2), ter(terNO_ACCOUNT)); - checkMetrics(__LINE__, env, 1, 10, 6, 5); + checkMetrics(*this, env, 1, 10, 6, 5); env.close(); - checkMetrics(__LINE__, env, 0, 12, 1, 6); + checkMetrics(*this, env, 0, 12, 1, 6); env.require(balance(iris, XRP(1000))); BEAST_EXPECT(env.seq(iris) == 11); @@ -475,7 +351,7 @@ public: ++metrics.txCount; checkMetrics( - __LINE__, + *this, env, metrics.txCount, metrics.txQMaxSize, @@ -496,14 +372,14 @@ public: auto queued = ter(terQUEUED); auto const baseFee = env.current()->fees().base.drops(); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 3); + checkMetrics(*this, env, 0, std::nullopt, 0, 3); // Fund alice and then fill the ledger. env.fund(XRP(50000), noripple(alice)); env(noop(alice)); env(noop(alice)); env(noop(alice)); - checkMetrics(__LINE__, env, 0, std::nullopt, 4, 3); + checkMetrics(*this, env, 0, std::nullopt, 4, 3); ////////////////////////////////////////////////////////////////// @@ -515,11 +391,11 @@ public: env(noop(alice), ticket::use(tkt1 - 2), ter(tefNO_TICKET)); env(noop(alice), ticket::use(tkt1 - 1), ter(terPRE_TICKET)); env.require(owners(alice, 0), tickets(alice, 0)); - checkMetrics(__LINE__, env, 1, std::nullopt, 4, 3); + checkMetrics(*this, env, 1, std::nullopt, 4, 3); env.close(); env.require(owners(alice, 250), tickets(alice, 250)); - checkMetrics(__LINE__, env, 0, 8, 1, 4); + checkMetrics(*this, env, 0, 8, 1, 4); BEAST_EXPECT(env.seq(alice) == tkt1 + 250); ////////////////////////////////////////////////////////////////// @@ -547,7 +423,7 @@ public: ticket::use(tkt1 + 13), fee(baseFee * 2.3), ter(telCAN_NOT_QUEUE_FULL)); - checkMetrics(__LINE__, env, 8, 8, 5, 4, expectedMinFeeLevel); + checkMetrics(*this, env, 8, 8, 5, 4, expectedMinFeeLevel); // Check which of the queued transactions got into the ledger by // attempting to replace them. @@ -579,7 +455,7 @@ public: // the queue. env(noop(alice), ticket::use(tkt1 + 13), ter(telCAN_NOT_QUEUE_FEE)); - checkMetrics(__LINE__, env, 3, 10, 6, 5); + checkMetrics(*this, env, 3, 10, 6, 5); ////////////////////////////////////////////////////////////////// @@ -610,7 +486,7 @@ public: env(noop(alice), seq(nextSeq + 5), queued); env(noop(alice), seq(nextSeq + 6), queued); env(noop(alice), seq(nextSeq + 7), ter(telCAN_NOT_QUEUE_FULL)); - checkMetrics(__LINE__, env, 10, 10, 6, 5, 257); + checkMetrics(*this, env, 10, 10, 6, 5, 257); // Check which of the queued transactions got into the ledger by // attempting to replace them. @@ -638,7 +514,7 @@ public: env(noop(alice), seq(nextSeq + 6), ter(telCAN_NOT_QUEUE_FEE)); env(noop(alice), seq(nextSeq + 7), ter(telCAN_NOT_QUEUE_FEE)); - checkMetrics(__LINE__, env, 4, 12, 7, 6); + checkMetrics(*this, env, 4, 12, 7, 6); BEAST_EXPECT(env.seq(alice) == nextSeq + 4); ////////////////////////////////////////////////////////////////// @@ -669,7 +545,7 @@ public: fee(baseFee * 2.1), ter(telCAN_NOT_QUEUE_FULL)); - checkMetrics(__LINE__, env, 10, 12, 7, 6); + checkMetrics(*this, env, 10, 12, 7, 6); env.close(); env.require(owners(alice, 231), tickets(alice, 231)); @@ -700,7 +576,7 @@ public: env(noop(alice), seq(nextSeq + 7), ter(telCAN_NOT_QUEUE_FEE)); BEAST_EXPECT(env.seq(alice) == nextSeq + 6); - checkMetrics(__LINE__, env, 6, 14, 8, 7); + checkMetrics(*this, env, 6, 14, 8, 7); ////////////////////////////////////////////////////////////////// @@ -739,7 +615,7 @@ public: env(noop(alice), seq(nextSeq + 7), ter(tefPAST_SEQ)); BEAST_EXPECT(env.seq(alice) == nextSeq + 8); - checkMetrics(__LINE__, env, 0, 16, 6, 8); + checkMetrics(*this, env, 0, 16, 6, 8); } void @@ -754,28 +630,28 @@ public: auto gw = Account("gw"); auto USD = gw["USD"]; - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 2); + checkMetrics(*this, env, 0, std::nullopt, 0, 2); // Create accounts env.fund(XRP(50000), noripple(alice, gw)); - checkMetrics(__LINE__, env, 0, std::nullopt, 2, 2); + checkMetrics(*this, env, 0, std::nullopt, 2, 2); env.close(); - checkMetrics(__LINE__, env, 0, 4, 0, 2); + checkMetrics(*this, env, 0, 4, 0, 2); // Alice creates an unfunded offer while the ledger is not full env(offer(alice, XRP(1000), USD(1000)), ter(tecUNFUNDED_OFFER)); - checkMetrics(__LINE__, env, 0, 4, 1, 2); + checkMetrics(*this, env, 0, 4, 1, 2); fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, 4, 3, 2); + checkMetrics(*this, env, 0, 4, 3, 2); // Alice creates an unfunded offer that goes in the queue env(offer(alice, XRP(1000), USD(1000)), ter(terQUEUED)); - checkMetrics(__LINE__, env, 1, 4, 3, 2); + checkMetrics(*this, env, 1, 4, 3, 2); // The offer comes out of the queue env.close(); - checkMetrics(__LINE__, env, 0, 6, 1, 3); + checkMetrics(*this, env, 0, 6, 1, 3); } void @@ -794,44 +670,44 @@ public: auto queued = ter(terQUEUED); auto const baseFee = env.current()->fees().base.drops(); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 2); + checkMetrics(*this, env, 0, std::nullopt, 0, 2); // Create several accounts while the fee is cheap so they all apply. env.fund(XRP(50000), noripple(alice, bob, charlie)); - checkMetrics(__LINE__, env, 0, std::nullopt, 3, 2); + checkMetrics(*this, env, 0, std::nullopt, 3, 2); // Future transaction for Alice - fails env(noop(alice), fee(openLedgerCost(env)), seq(env.seq(alice) + 1), ter(terPRE_SEQ)); - checkMetrics(__LINE__, env, 0, std::nullopt, 3, 2); + checkMetrics(*this, env, 0, std::nullopt, 3, 2); // Current transaction for Alice: held env(noop(alice), queued); - checkMetrics(__LINE__, env, 1, std::nullopt, 3, 2); + checkMetrics(*this, env, 1, std::nullopt, 3, 2); // Alice - sequence is too far ahead, so won't queue. env(noop(alice), seq(env.seq(alice) + 2), ter(telCAN_NOT_QUEUE)); - checkMetrics(__LINE__, env, 1, std::nullopt, 3, 2); + checkMetrics(*this, env, 1, std::nullopt, 3, 2); // Bob with really high fee - applies env(noop(bob), fee(openLedgerCost(env))); - checkMetrics(__LINE__, env, 1, std::nullopt, 4, 2); + checkMetrics(*this, env, 1, std::nullopt, 4, 2); // Daria with low fee: hold env(noop(charlie), fee(baseFee * 100), queued); - checkMetrics(__LINE__, env, 2, std::nullopt, 4, 2); + checkMetrics(*this, env, 2, std::nullopt, 4, 2); // Alice with normal fee: hold env(noop(alice), seq(env.seq(alice) + 1), queued); - checkMetrics(__LINE__, env, 3, std::nullopt, 4, 2); + checkMetrics(*this, env, 3, std::nullopt, 4, 2); env.close(); // Verify that the held transactions got applied // Alice's bad transaction applied from the // Local Txs. - checkMetrics(__LINE__, env, 0, 8, 4, 4); + checkMetrics(*this, env, 0, 8, 4, 4); } void @@ -853,7 +729,7 @@ public: auto queued = ter(terQUEUED); auto const baseFee = env.current()->fees().base.drops(); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 2); + checkMetrics(*this, env, 0, std::nullopt, 0, 2); // Fund across several ledgers so the TxQ metrics stay restricted. env.fund(XRP(1000), noripple(alice, bob)); @@ -863,11 +739,11 @@ public: env.fund(XRP(1000), noripple(edgar, felicia)); env.close(env.now() + 5s, 10000ms); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 2); + checkMetrics(*this, env, 0, std::nullopt, 0, 2); env(noop(bob)); env(noop(charlie)); env(noop(daria)); - checkMetrics(__LINE__, env, 0, std::nullopt, 3, 2); + checkMetrics(*this, env, 0, std::nullopt, 3, 2); BEAST_EXPECT(env.current()->info().seq == 6); // Fail to queue an item with a low LastLedgerSeq @@ -886,7 +762,7 @@ public: env(noop(charlie), fee(largeFee), queued); env(noop(daria), fee(largeFee), queued); env(noop(edgar), fee(largeFee), queued); - checkMetrics(__LINE__, env, 5, std::nullopt, 3, 2); + checkMetrics(*this, env, 5, std::nullopt, 3, 2); { auto& txQ = env.app().getTxQ(); auto aliceStat = txQ.getAccountTxs(alice.id()); @@ -910,7 +786,7 @@ public: } env.close(); - checkMetrics(__LINE__, env, 1, 6, 4, 3); + checkMetrics(*this, env, 1, 6, 4, 3); // Keep alice's transaction waiting. env(noop(bob), fee(largeFee), queued); @@ -918,12 +794,12 @@ public: env(noop(daria), fee(largeFee), queued); env(noop(edgar), fee(largeFee), queued); env(noop(felicia), fee(largeFee - 1), queued); - checkMetrics(__LINE__, env, 6, 6, 4, 3, 257); + checkMetrics(*this, env, 6, 6, 4, 3, 257); env.close(); // alice's transaction is still hanging around // clang-format off - checkMetrics(__LINE__, env, 1, 8, 5, 4, baseFeeLevel.fee(), baseFeeLevel.fee() * largeFeeMultiplier); + checkMetrics(*this, env, 1, 8, 5, 4, baseFeeLevel.fee(), baseFeeLevel.fee() * largeFeeMultiplier); // clang-format on BEAST_EXPECT(env.seq(alice) == 3); @@ -938,7 +814,7 @@ public: env(noop(edgar), fee(anotherLargeFee), queued); env(noop(felicia), fee(anotherLargeFee - 1), queued); env(noop(felicia), fee(anotherLargeFee - 1), seq(env.seq(felicia) + 1), queued); - checkMetrics(__LINE__, env, 8, 8, 5, 4, baseFeeLevel.fee() + 1, baseFeeLevel.fee() * largeFeeMultiplier); + checkMetrics(*this, env, 8, 8, 5, 4, baseFeeLevel.fee() + 1, baseFeeLevel.fee() * largeFeeMultiplier); // clang-format on env.close(); @@ -946,7 +822,7 @@ public: // into the ledger, so her transaction is gone, // though one of felicia's is still in the queue. // clang-format off - checkMetrics(__LINE__, env, 1, 10, 6, 5, baseFeeLevel.fee(), baseFeeLevel.fee() * largeFeeMultiplier); + checkMetrics(*this, env, 1, 10, 6, 5, baseFeeLevel.fee(), baseFeeLevel.fee() * largeFeeMultiplier); // clang-format on BEAST_EXPECT(env.seq(alice) == 3); BEAST_EXPECT(env.seq(felicia) == 7); @@ -954,7 +830,7 @@ public: env.close(); // And now the queue is empty // clang-format off - checkMetrics(__LINE__, env, 0, 12, 1, 6, baseFeeLevel.fee(), baseFeeLevel.fee() * anotherLargeFeeMultiplier); + checkMetrics(*this, env, 0, 12, 1, 6, baseFeeLevel.fee(), baseFeeLevel.fee() * anotherLargeFeeMultiplier); // clang-format on BEAST_EXPECT(env.seq(alice) == 3); BEAST_EXPECT(env.seq(felicia) == 8); @@ -976,7 +852,7 @@ public: auto queued = ter(terQUEUED); auto const baseFee = env.current()->fees().base.drops(); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 2); + checkMetrics(*this, env, 0, std::nullopt, 0, 2); // Fund across several ledgers so the TxQ metrics stay restricted. env.fund(XRP(1000), noripple(alice, bob)); @@ -988,21 +864,21 @@ public: env(noop(alice)); env(noop(alice)); env(noop(alice)); - checkMetrics(__LINE__, env, 0, std::nullopt, 3, 2); + checkMetrics(*this, env, 0, std::nullopt, 3, 2); env(noop(bob), queued); - checkMetrics(__LINE__, env, 1, std::nullopt, 3, 2); + checkMetrics(*this, env, 1, std::nullopt, 3, 2); // Since Alice's queue is empty this blocker can go into her queue. env(regkey(alice, bob), fee(0), queued); - checkMetrics(__LINE__, env, 2, std::nullopt, 3, 2); + checkMetrics(*this, env, 2, std::nullopt, 3, 2); // Close out this ledger so we can get a maxsize env.close(); - checkMetrics(__LINE__, env, 0, 6, 2, 3); + checkMetrics(*this, env, 0, 6, 2, 3); fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, 6, 4, 3); + checkMetrics(*this, env, 0, 6, 4, 3); constexpr auto aliceFeeMultiplier = 3; auto feeAlice = baseFee * aliceFeeMultiplier; @@ -1013,12 +889,12 @@ public: feeAlice = (feeAlice + 1) * 125 / 100; ++seqAlice; } - checkMetrics(__LINE__, env, 4, 6, 4, 3); + checkMetrics(*this, env, 4, 6, 4, 3); // Bob adds a zero fee blocker to his queue. auto const seqBob = env.seq(bob); env(regkey(bob, alice), fee(0), queued); - checkMetrics(__LINE__, env, 5, 6, 4, 3); + checkMetrics(*this, env, 5, 6, 4, 3); // Carol fills the queue. auto feeCarol = feeAlice; @@ -1030,7 +906,7 @@ public: ++seqCarol; } // clang-format off - checkMetrics( __LINE__, env, 6, 6, 4, 3, baseFeeLevel.fee() * aliceFeeMultiplier + 1); + checkMetrics(*this, env, 6, 6, 4, 3, baseFeeLevel.fee() * aliceFeeMultiplier + 1); // clang-format on // Carol submits high enough to beat Bob's average fee which kicks @@ -1042,20 +918,20 @@ public: env.close(); // Some of Alice's transactions stay in the queue. Bob's // transaction returns to the TxQ. - checkMetrics(__LINE__, env, 5, 8, 5, 4); + checkMetrics(*this, env, 5, 8, 5, 4); BEAST_EXPECT(env.seq(alice) == seqAlice - 4); BEAST_EXPECT(env.seq(bob) == seqBob); BEAST_EXPECT(env.seq(carol) == seqCarol + 1); env.close(); // The remaining queued transactions flush through to the ledger. - checkMetrics(__LINE__, env, 0, 10, 5, 5); + checkMetrics(*this, env, 0, 10, 5, 5); BEAST_EXPECT(env.seq(alice) == seqAlice); BEAST_EXPECT(env.seq(bob) == seqBob + 1); BEAST_EXPECT(env.seq(carol) == seqCarol + 1); env.close(); - checkMetrics(__LINE__, env, 0, 10, 0, 5); + checkMetrics(*this, env, 0, 10, 0, 5); BEAST_EXPECT(env.seq(alice) == seqAlice); BEAST_EXPECT(env.seq(bob) == seqBob + 1); BEAST_EXPECT(env.seq(carol) == seqCarol + 1); @@ -1101,19 +977,19 @@ public: auto queued = ter(terQUEUED); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 2); + checkMetrics(*this, env, 0, std::nullopt, 0, 2); env.fund(XRP(1000), noripple(alice, bob)); - checkMetrics(__LINE__, env, 0, std::nullopt, 2, 2); + checkMetrics(*this, env, 0, std::nullopt, 2, 2); // Fill the ledger env(noop(alice)); - checkMetrics(__LINE__, env, 0, std::nullopt, 3, 2); + checkMetrics(*this, env, 0, std::nullopt, 3, 2); // Put a transaction in the queue env(noop(alice), queued); - checkMetrics(__LINE__, env, 1, std::nullopt, 3, 2); + checkMetrics(*this, env, 1, std::nullopt, 3, 2); // Now cheat, and bypass the queue. { @@ -1131,12 +1007,12 @@ public: }); env.postconditions(jt, parsed); } - checkMetrics(__LINE__, env, 1, std::nullopt, 4, 2); + checkMetrics(*this, env, 1, std::nullopt, 4, 2); env.close(); // Alice's queued transaction failed in TxQ::accept // with tefPAST_SEQ - checkMetrics(__LINE__, env, 0, 8, 0, 4); + checkMetrics(*this, env, 0, 8, 0, 4); } void @@ -1158,7 +1034,7 @@ public: auto queued = ter(terQUEUED); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 3); + checkMetrics(*this, env, 0, std::nullopt, 0, 3); // ledgers in queue is 2 because of makeConfig auto const initQueueMax = initFee(env, 3, 2, 10, 200, 50); @@ -1166,11 +1042,11 @@ public: // Create several accounts while the fee is cheap so they all apply. env.fund(drops(2000), noripple(alice)); env.fund(XRP(500000), noripple(bob, charlie, daria)); - checkMetrics(__LINE__, env, 0, initQueueMax, 4, 3); + checkMetrics(*this, env, 0, initQueueMax, 4, 3); // Alice - price starts exploding: held env(noop(alice), fee(11), queued); - checkMetrics(__LINE__, env, 1, initQueueMax, 4, 3); + checkMetrics(*this, env, 1, initQueueMax, 4, 3); auto aliceSeq = env.seq(alice); auto bobSeq = env.seq(bob); @@ -1178,28 +1054,28 @@ public: // Alice - try to queue a second transaction, but leave a gap env(noop(alice), seq(aliceSeq + 2), fee(100), ter(telCAN_NOT_QUEUE)); - checkMetrics(__LINE__, env, 1, initQueueMax, 4, 3); + checkMetrics(*this, env, 1, initQueueMax, 4, 3); // Alice - queue a second transaction. Yay! env(noop(alice), seq(aliceSeq + 1), fee(13), queued); - checkMetrics(__LINE__, env, 2, initQueueMax, 4, 3); + checkMetrics(*this, env, 2, initQueueMax, 4, 3); // Alice - queue a third transaction. Yay. env(noop(alice), seq(aliceSeq + 2), fee(17), queued); - checkMetrics(__LINE__, env, 3, initQueueMax, 4, 3); + checkMetrics(*this, env, 3, initQueueMax, 4, 3); // Bob - queue a transaction env(noop(bob), queued); - checkMetrics(__LINE__, env, 4, initQueueMax, 4, 3); + checkMetrics(*this, env, 4, initQueueMax, 4, 3); // Bob - queue a second transaction env(noop(bob), seq(bobSeq + 1), fee(50), queued); - checkMetrics(__LINE__, env, 5, initQueueMax, 4, 3); + checkMetrics(*this, env, 5, initQueueMax, 4, 3); // Charlie - queue a transaction, with a higher fee // than default env(noop(charlie), fee(15), queued); - checkMetrics(__LINE__, env, 6, initQueueMax, 4, 3); + checkMetrics(*this, env, 6, initQueueMax, 4, 3); BEAST_EXPECT(env.seq(alice) == aliceSeq); BEAST_EXPECT(env.seq(bob) == bobSeq); @@ -1208,7 +1084,7 @@ public: env.close(); // Verify that all of but one of the queued transactions // got applied. - checkMetrics(__LINE__, env, 1, 8, 5, 4); + checkMetrics(*this, env, 1, 8, 5, 4); // Verify that the stuck transaction is Bob's second. // Even though it had a higher fee than Alice's and @@ -1230,7 +1106,7 @@ public: queued); ++aliceSeq; } - checkMetrics(__LINE__, env, 8, 8, 5, 4, 513); + checkMetrics(*this, env, 8, 8, 5, 4, 513); { auto& txQ = env.app().getTxQ(); auto aliceStat = txQ.getAccountTxs(alice.id()); @@ -1261,24 +1137,24 @@ public: json(jss::LastLedgerSequence, lastLedgerSeq + 7), fee(aliceFee), ter(telCAN_NOT_QUEUE_FULL)); - checkMetrics(__LINE__, env, 8, 8, 5, 4, 513); + checkMetrics(*this, env, 8, 8, 5, 4, 513); // Charlie - try to add another item to the queue, // which fails because fee is lower than Alice's // queued average. env(noop(charlie), fee(19), ter(telCAN_NOT_QUEUE_FULL)); - checkMetrics(__LINE__, env, 8, 8, 5, 4, 513); + checkMetrics(*this, env, 8, 8, 5, 4, 513); // Charlie - add another item to the queue, which // causes Alice's last txn to drop env(noop(charlie), fee(30), queued); - checkMetrics(__LINE__, env, 8, 8, 5, 4, 538); + checkMetrics(*this, env, 8, 8, 5, 4, 538); // Alice - now attempt to add one more to the queue, // which fails because the last tx was dropped, so // there is no complete chain. env(noop(alice), seq(aliceSeq), fee(aliceFee), ter(telCAN_NOT_QUEUE)); - checkMetrics(__LINE__, env, 8, 8, 5, 4, 538); + checkMetrics(*this, env, 8, 8, 5, 4, 538); // Alice wants this tx more than the dropped tx, // so resubmits with higher fee, but the queue @@ -1287,7 +1163,7 @@ public: seq(aliceSeq - 1), fee(aliceFee), ter(telCAN_NOT_QUEUE_FULL)); - checkMetrics(__LINE__, env, 8, 8, 5, 4, 538); + checkMetrics(*this, env, 8, 8, 5, 4, 538); // Try to replace a middle item in the queue // without enough fee. @@ -1297,18 +1173,18 @@ public: seq(aliceSeq), fee(aliceFee), ter(telCAN_NOT_QUEUE_FEE)); - checkMetrics(__LINE__, env, 8, 8, 5, 4, 538); + checkMetrics(*this, env, 8, 8, 5, 4, 538); // Replace a middle item from the queue successfully ++aliceFee; env(noop(alice), seq(aliceSeq), fee(aliceFee), queued); - checkMetrics(__LINE__, env, 8, 8, 5, 4, 538); + checkMetrics(*this, env, 8, 8, 5, 4, 538); env.close(); // Alice's transactions processed, along with // Charlie's, and the lost one is replayed and // added back to the queue. - checkMetrics(__LINE__, env, 4, 10, 6, 5); + checkMetrics(*this, env, 4, 10, 6, 5); aliceSeq = env.seq(alice) + 1; @@ -1322,18 +1198,18 @@ public: seq(aliceSeq), fee(aliceFee), ter(telCAN_NOT_QUEUE_BALANCE)); - checkMetrics(__LINE__, env, 4, 10, 6, 5); + checkMetrics(*this, env, 4, 10, 6, 5); // Try to spend more than Alice can afford with all the other txs. aliceSeq += 2; env(noop(alice), seq(aliceSeq), fee(aliceFee), ter(terINSUF_FEE_B)); - checkMetrics(__LINE__, env, 4, 10, 6, 5); + checkMetrics(*this, env, 4, 10, 6, 5); // Replace the last queued item with a transaction that will // bankrupt Alice --aliceFee; env(noop(alice), seq(aliceSeq), fee(aliceFee), queued); - checkMetrics(__LINE__, env, 4, 10, 6, 5); + checkMetrics(*this, env, 4, 10, 6, 5); // Alice - Attempt to queue a last transaction, but it // fails because the fee in flight is too high, before @@ -1344,14 +1220,14 @@ public: seq(aliceSeq), fee(aliceFee), ter(telCAN_NOT_QUEUE_BALANCE)); - checkMetrics(__LINE__, env, 4, 10, 6, 5); + checkMetrics(*this, env, 4, 10, 6, 5); env.close(); // All of Alice's transactions applied. - checkMetrics(__LINE__, env, 0, 12, 4, 6); + checkMetrics(*this, env, 0, 12, 4, 6); env.close(); - checkMetrics(__LINE__, env, 0, 12, 0, 6); + checkMetrics(*this, env, 0, 12, 0, 6); // Alice is broke env.require(balance(alice, XRP(0))); @@ -1361,17 +1237,17 @@ public: // account limit (10) txs. fillQueue(env, bob); bobSeq = env.seq(bob); - checkMetrics(__LINE__, env, 0, 12, 7, 6); + checkMetrics(*this, env, 0, 12, 7, 6); for (int i = 0; i < 10; ++i) env(noop(bob), seq(bobSeq + i), queued); - checkMetrics(__LINE__, env, 10, 12, 7, 6); + checkMetrics(*this, env, 10, 12, 7, 6); // Bob hit the single account limit env(noop(bob), seq(bobSeq + 10), ter(telCAN_NOT_QUEUE_FULL)); - checkMetrics(__LINE__, env, 10, 12, 7, 6); + checkMetrics(*this, env, 10, 12, 7, 6); // Bob can replace one of the earlier txs regardless // of the limit env(noop(bob), seq(bobSeq + 5), fee(20), queued); - checkMetrics(__LINE__, env, 10, 12, 7, 6); + checkMetrics(*this, env, 10, 12, 7, 6); // Try to replace a middle item in the queue // with enough fee to bankrupt bob and make the @@ -1382,7 +1258,7 @@ public: seq(bobSeq + 5), fee(bobFee), ter(telCAN_NOT_QUEUE_BALANCE)); - checkMetrics(__LINE__, env, 10, 12, 7, 6); + checkMetrics(*this, env, 10, 12, 7, 6); // Attempt to replace a middle item in the queue with enough fee // to bankrupt bob, and also to use fee averaging to clear out the @@ -1396,14 +1272,14 @@ public: seq(bobSeq + 5), fee(bobFee), ter(telCAN_NOT_QUEUE_BALANCE)); - checkMetrics(__LINE__, env, 10, 12, 7, 6); + checkMetrics(*this, env, 10, 12, 7, 6); // Close the ledger and verify that the queued transactions succeed // and bob has the right ending balance. env.close(); - checkMetrics(__LINE__, env, 3, 14, 8, 7); + checkMetrics(*this, env, 3, 14, 8, 7); env.close(); - checkMetrics(__LINE__, env, 0, 16, 3, 8); + checkMetrics(*this, env, 0, 16, 3, 8); env.require(balance(bob, drops(499'999'999'750))); } @@ -1431,20 +1307,20 @@ public: BEAST_EXPECT(env.current()->fees().base == 10); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 4); + checkMetrics(*this, env, 0, std::nullopt, 0, 4); // Create several accounts while the fee is cheap so they all apply. env.fund(XRP(50000), noripple(alice, bob, charlie, daria)); - checkMetrics(__LINE__, env, 0, std::nullopt, 4, 4); + checkMetrics(*this, env, 0, std::nullopt, 4, 4); env.close(); - checkMetrics(__LINE__, env, 0, 8, 0, 4); + checkMetrics(*this, env, 0, 8, 0, 4); env.fund(XRP(50000), noripple(elmo, fred, gwen, hank)); - checkMetrics(__LINE__, env, 0, 8, 4, 4); + checkMetrics(*this, env, 0, 8, 4, 4); env.close(); - checkMetrics(__LINE__, env, 0, 8, 0, 4); + checkMetrics(*this, env, 0, 8, 0, 4); ////////////////////////////////////////////////////////////// @@ -1455,7 +1331,7 @@ public: env(noop(gwen)); env(noop(fred)); env(noop(elmo)); - checkMetrics(__LINE__, env, 0, 8, 5, 4); + checkMetrics(*this, env, 0, 8, 5, 4); auto aliceSeq = env.seq(alice); auto bobSeq = env.seq(bob); @@ -1482,7 +1358,7 @@ public: // Queue is full now. Minimum fee now reflects the // lowest fee in the queue. auto minFeeLevel = txFeeLevelByAccount(env, alice); - checkMetrics(__LINE__, env, 8, 8, 5, 4, minFeeLevel + 1); + checkMetrics(*this, env, 8, 8, 5, 4, minFeeLevel + 1); // Try to add another transaction with the default (low) fee, // it should fail because it can't replace the one already @@ -1495,13 +1371,13 @@ public: env(noop(charlie), fee(100), seq(charlieSeq + 1), queued); // Queue is still full. - checkMetrics(__LINE__, env, 8, 8, 5, 4, minFeeLevel + 1); + checkMetrics(*this, env, 8, 8, 5, 4, minFeeLevel + 1); // Six txs are processed out of the queue into the ledger, // leaving two txs. The dropped tx is retried from localTxs, and // put back into the queue. env.close(); - checkMetrics(__LINE__, env, 3, 10, 6, 5); + checkMetrics(*this, env, 3, 10, 6, 5); // This next test should remain unchanged regardless of // transaction ordering @@ -1587,7 +1463,7 @@ public: env(noop(gwen), seq(gwenSeq + qTxCount1[gwen.id()]++), fee(15), queued); minFeeLevel = txFeeLevelByAccount(env, gwen) + 1; - checkMetrics(__LINE__, env, 10, 10, 6, 5, minFeeLevel); + checkMetrics(*this, env, 10, 10, 6, 5, minFeeLevel); // Add another transaction, with a higher fee, // Not high enough to get into the ledger, but high @@ -1597,13 +1473,13 @@ public: seq(aliceSeq + qTxCount1[alice.id()]++), queued); - checkMetrics(__LINE__, env, 10, 10, 6, 5, minFeeLevel); + checkMetrics(*this, env, 10, 10, 6, 5, minFeeLevel); // Seven txs are processed out of the queue, leaving 3. One // dropped tx is retried from localTxs, and put back into the // queue. env.close(); - checkMetrics(__LINE__, env, 4, 12, 7, 6); + checkMetrics(*this, env, 4, 12, 7, 6); // Refresh the queue counts auto qTxCount2 = getTxsQueued(); @@ -1668,13 +1544,13 @@ public: auto alice = Account("alice"); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 1); + checkMetrics(*this, env, 0, std::nullopt, 0, 1); env.fund(XRP(50000), noripple(alice)); - checkMetrics(__LINE__, env, 0, std::nullopt, 1, 1); + checkMetrics(*this, env, 0, std::nullopt, 1, 1); env(fset(alice, asfAccountTxnID)); - checkMetrics(__LINE__, env, 0, std::nullopt, 2, 1); + checkMetrics(*this, env, 0, std::nullopt, 2, 1); // Immediately after the fset, the sfAccountTxnID field // is still uninitialized, so preflight succeeds here, @@ -1683,14 +1559,14 @@ public: json(R"({"AccountTxnID": "0"})"), ter(telCAN_NOT_QUEUE)); - checkMetrics(__LINE__, env, 0, std::nullopt, 2, 1); + checkMetrics(*this, env, 0, std::nullopt, 2, 1); env.close(); // The failed transaction is retried from LocalTx // and succeeds. - checkMetrics(__LINE__, env, 0, 4, 1, 2); + checkMetrics(*this, env, 0, 4, 1, 2); env(noop(alice)); - checkMetrics(__LINE__, env, 0, 4, 2, 2); + checkMetrics(*this, env, 0, 4, 2, 2); env(noop(alice), json(R"({"AccountTxnID": "0"})"), ter(tefWRONG_PRIOR)); } @@ -1714,10 +1590,10 @@ public: auto alice = Account("alice"); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 2); + checkMetrics(*this, env, 0, std::nullopt, 0, 2); env.fund(XRP(50000), noripple(alice)); - checkMetrics(__LINE__, env, 0, std::nullopt, 1, 2); + checkMetrics(*this, env, 0, std::nullopt, 1, 2); FeeLevel64 medFeeLevel; for (int i = 0; i < 10; ++i) @@ -1737,12 +1613,12 @@ public: env(noop(alice), fee(cost)); } - checkMetrics(__LINE__, env, 0, std::nullopt, 11, 2); + checkMetrics(*this, env, 0, std::nullopt, 11, 2); env.close(); // If not for the maximum, the per ledger would be 11. // clang-format off - checkMetrics(__LINE__, env, 0, 10, 0, 5, baseFeeLevel.fee(), calcMedFeeLevel(medFeeLevel)); + checkMetrics(*this, env, 0, 10, 0, 5, baseFeeLevel.fee(), calcMedFeeLevel(medFeeLevel)); // clang-format on } @@ -1831,22 +1707,22 @@ public: // ledgers in queue is 2 because of makeConfig auto const initQueueMax = initFee(env, 3, 2, 10, 200, 50); - checkMetrics(__LINE__, env, 0, initQueueMax, 0, 3); + checkMetrics(*this, env, 0, initQueueMax, 0, 3); env.fund(drops(5000), noripple(alice)); env.fund(XRP(50000), noripple(bob)); - checkMetrics(__LINE__, env, 0, initQueueMax, 2, 3); + checkMetrics(*this, env, 0, initQueueMax, 2, 3); auto USD = bob["USD"]; env(offer(alice, USD(5000), drops(5000)), require(owners(alice, 1))); - checkMetrics(__LINE__, env, 0, initQueueMax, 3, 3); + checkMetrics(*this, env, 0, initQueueMax, 3, 3); env.close(); - checkMetrics(__LINE__, env, 0, 6, 0, 3); + checkMetrics(*this, env, 0, 6, 0, 3); // Fill up the ledger fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, 6, 4, 3); + checkMetrics(*this, env, 0, 6, 4, 3); // Queue up a couple of transactions, plus one // more expensive one. @@ -1855,7 +1731,7 @@ public: env(noop(alice), seq(aliceSeq++), queued); env(noop(alice), seq(aliceSeq++), queued); env(noop(alice), fee(drops(1000)), seq(aliceSeq), queued); - checkMetrics(__LINE__, env, 4, 6, 4, 3); + checkMetrics(*this, env, 4, 6, 4, 3); // This offer should take Alice's offer // up to Alice's reserve. @@ -1863,7 +1739,7 @@ public: fee(openLedgerCost(env)), require( balance(alice, drops(250)), owners(alice, 1), lines(alice, 1))); - checkMetrics(__LINE__, env, 4, 6, 5, 3); + checkMetrics(*this, env, 4, 6, 5, 3); // Try adding a new transaction. // Too many fees in flight. @@ -1871,12 +1747,12 @@ public: fee(drops(200)), seq(aliceSeq + 1), ter(telCAN_NOT_QUEUE_BALANCE)); - checkMetrics(__LINE__, env, 4, 6, 5, 3); + checkMetrics(*this, env, 4, 6, 5, 3); // Close the ledger. All of Alice's transactions // take a fee, except the last one. env.close(); - checkMetrics(__LINE__, env, 1, 10, 3, 5); + checkMetrics(*this, env, 1, 10, 3, 5); env.require(balance(alice, drops(250 - 30))); // Still can't add a new transaction for Alice, @@ -1885,7 +1761,7 @@ public: fee(drops(200)), seq(aliceSeq + 1), ter(telCAN_NOT_QUEUE_BALANCE)); - checkMetrics(__LINE__, env, 1, 10, 3, 5); + checkMetrics(*this, env, 1, 10, 3, 5); /* At this point, Alice's transaction is indefinitely stuck in the queue. Eventually it will either @@ -1897,13 +1773,13 @@ public: for (int i = 0; i < 9; ++i) { env.close(); - checkMetrics(__LINE__, env, 1, 10, 0, 5); + checkMetrics(*this, env, 1, 10, 0, 5); } // And Alice's transaction expires (via the retry limit, // not LastLedgerSequence). env.close(); - checkMetrics(__LINE__, env, 0, 10, 0, 5); + checkMetrics(*this, env, 0, 10, 0, 5); } void @@ -1922,11 +1798,11 @@ public: Env env(*this, makeConfig({{"minimum_txn_in_ledger_standalone", "3"}})); auto const baseFee = env.current()->fees().base.drops(); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 3); + checkMetrics(*this, env, 0, std::nullopt, 0, 3); env.fund(XRP(50000), noripple(alice, bob)); env.memoize(charlie); - checkMetrics(__LINE__, env, 0, std::nullopt, 2, 3); + checkMetrics(*this, env, 0, std::nullopt, 2, 3); { // Cannot put a blocker in an account's queue if that queue // already holds two or more (non-blocker) entries. @@ -1935,7 +1811,7 @@ public: env(noop(alice)); // Set a regular key just to clear the password spent flag env(regkey(alice, charlie)); - checkMetrics(__LINE__, env, 0, std::nullopt, 4, 3); + checkMetrics(*this, env, 0, std::nullopt, 4, 3); // Put two "normal" txs in the queue auto const aliceSeq = env.seq(alice); @@ -1961,11 +1837,11 @@ public: // Other accounts are not affected env(noop(bob), queued); - checkMetrics(__LINE__, env, 3, std::nullopt, 4, 3); + checkMetrics(*this, env, 3, std::nullopt, 4, 3); // Drain the queue. env.close(); - checkMetrics(__LINE__, env, 0, 8, 4, 4); + checkMetrics(*this, env, 0, 8, 4, 4); } { // Replace a lone non-blocking tx with a blocker. @@ -2006,7 +1882,7 @@ public: // Drain the queue. env.close(); - checkMetrics(__LINE__, env, 0, 10, 3, 5); + checkMetrics(*this, env, 0, 10, 3, 5); } { // Put a blocker in an empty queue. @@ -2034,7 +1910,7 @@ public: // Drain the queue. env.close(); - checkMetrics(__LINE__, env, 0, 12, 3, 6); + checkMetrics(*this, env, 0, 12, 3, 6); } } @@ -2054,12 +1930,12 @@ public: Env env(*this, makeConfig({{"minimum_txn_in_ledger_standalone", "3"}})); auto const baseFee = env.current()->fees().base.drops(); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 3); + checkMetrics(*this, env, 0, std::nullopt, 0, 3); env.fund(XRP(50000), noripple(alice, bob)); env.memoize(charlie); - checkMetrics(__LINE__, env, 0, std::nullopt, 2, 3); + checkMetrics(*this, env, 0, std::nullopt, 2, 3); std::uint32_t tkt{env.seq(alice) + 1}; { @@ -2070,7 +1946,7 @@ public: env(ticket::create(alice, 250), seq(tkt - 1)); // Set a regular key just to clear the password spent flag env(regkey(alice, charlie)); - checkMetrics(__LINE__, env, 0, std::nullopt, 4, 3); + checkMetrics(*this, env, 0, std::nullopt, 4, 3); // Put two "normal" txs in the queue auto const aliceSeq = env.seq(alice); @@ -2100,11 +1976,11 @@ public: // Other accounts are not affected env(noop(bob), queued); - checkMetrics(__LINE__, env, 3, std::nullopt, 4, 3); + checkMetrics(*this, env, 3, std::nullopt, 4, 3); // Drain the queue and local transactions. env.close(); - checkMetrics(__LINE__, env, 0, 8, 5, 4); + checkMetrics(*this, env, 0, 8, 5, 4); // Show that the local transactions have flushed through as well. BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); @@ -2166,7 +2042,7 @@ public: // Drain the queue. env.close(); - checkMetrics(__LINE__, env, 0, 10, 4, 5); + checkMetrics(*this, env, 0, 10, 4, 5); // Show that the local transactions have flushed through as well. BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); @@ -2200,7 +2076,7 @@ public: // Drain the queue. env.close(); - checkMetrics(__LINE__, env, 0, 12, 3, 6); + checkMetrics(*this, env, 0, 12, 3, 6); } } @@ -2232,10 +2108,10 @@ public: auto limit = 3; - checkMetrics(__LINE__, env, 0, initQueueMax, 0, limit); + checkMetrics(*this, env, 0, initQueueMax, 0, limit); env.fund(XRP(50000), noripple(alice, charlie), gw); - checkMetrics(__LINE__, env, 0, initQueueMax, limit + 1, limit); + checkMetrics(*this, env, 0, initQueueMax, limit + 1, limit); auto USD = gw["USD"]; auto BUX = gw["BUX"]; @@ -2250,16 +2126,16 @@ public: // If this offer crosses, all of alice's // XRP will be taken (except the reserve). env(offer(alice, BUX(5000), XRP(50000)), queued); - checkMetrics(__LINE__, env, 1, initQueueMax, limit + 1, limit); + checkMetrics(*this, env, 1, initQueueMax, limit + 1, limit); // But because the reserve is protected, another // transaction will be allowed to queue env(noop(alice), seq(aliceSeq + 1), queued); - checkMetrics(__LINE__, env, 2, initQueueMax, limit + 1, limit); + checkMetrics(*this, env, 2, initQueueMax, limit + 1, limit); env.close(); ++limit; - checkMetrics(__LINE__, env, 0, limit * 2, 2, limit); + checkMetrics(*this, env, 0, limit * 2, 2, limit); // But once we close the ledger, we find alice // has plenty of XRP, because the offer didn't @@ -2271,7 +2147,7 @@ public: ////////////////////////////////////////// // Offer with high XRP out and high total fee blocks later txs fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 0, limit * 2, limit + 1, limit); aliceSeq = env.seq(alice); aliceBal = env.balance(alice); @@ -2279,12 +2155,12 @@ public: // Alice creates an offer with a fee of half the reserve env(offer(alice, BUX(5000), XRP(50000)), fee(drops(100)), queued); - checkMetrics(__LINE__, env, 1, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 1, limit * 2, limit + 1, limit); // Alice creates another offer with a fee // that brings the total to just shy of the reserve env(noop(alice), fee(drops(99)), seq(aliceSeq + 1), queued); - checkMetrics(__LINE__, env, 2, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 2, limit * 2, limit + 1, limit); // So even a noop will look like alice // doesn't have the balance to pay the fee @@ -2292,11 +2168,11 @@ public: fee(drops(51)), seq(aliceSeq + 2), ter(terINSUF_FEE_B)); - checkMetrics(__LINE__, env, 2, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 2, limit * 2, limit + 1, limit); env.close(); ++limit; - checkMetrics(__LINE__, env, 0, limit * 2, 3, limit); + checkMetrics(*this, env, 0, limit * 2, 3, limit); // But once we close the ledger, we find alice // has plenty of XRP, because the offer didn't @@ -2308,7 +2184,7 @@ public: ////////////////////////////////////////// // Offer with high XRP out and super high fee blocks later txs fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 0, limit * 2, limit + 1, limit); aliceSeq = env.seq(alice); aliceBal = env.balance(alice); @@ -2317,7 +2193,7 @@ public: // Alice creates an offer with a fee larger than the reserve // This one can queue because it's the first in the queue for alice env(offer(alice, BUX(5000), XRP(50000)), fee(drops(300)), queued); - checkMetrics(__LINE__, env, 1, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 1, limit * 2, limit + 1, limit); // So even a noop will look like alice // doesn't have the balance to pay the fee @@ -2325,11 +2201,11 @@ public: fee(drops(51)), seq(aliceSeq + 1), ter(telCAN_NOT_QUEUE_BALANCE)); - checkMetrics(__LINE__, env, 1, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 1, limit * 2, limit + 1, limit); env.close(); ++limit; - checkMetrics(__LINE__, env, 0, limit * 2, 2, limit); + checkMetrics(*this, env, 0, limit * 2, 2, limit); // But once we close the ledger, we find alice // has plenty of XRP, because the offer didn't @@ -2341,7 +2217,7 @@ public: ////////////////////////////////////////// // Offer with low XRP out allows later txs fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 0, limit * 2, limit + 1, limit); aliceSeq = env.seq(alice); aliceBal = env.balance(alice); @@ -2351,11 +2227,11 @@ public: // And later transactions are just fine env(noop(alice), seq(aliceSeq + 1), queued); - checkMetrics(__LINE__, env, 2, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 2, limit * 2, limit + 1, limit); env.close(); ++limit; - checkMetrics(__LINE__, env, 0, limit * 2, 2, limit); + checkMetrics(*this, env, 0, limit * 2, 2, limit); // But once we close the ledger, we find alice // has plenty of XRP, because the offer didn't @@ -2367,7 +2243,7 @@ public: ////////////////////////////////////////// // Large XRP payment doesn't block later txs fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 0, limit * 2, limit + 1, limit); aliceSeq = env.seq(alice); aliceBal = env.balance(alice); @@ -2380,11 +2256,11 @@ public: // But because the reserve is protected, another // transaction will be allowed to queue env(noop(alice), seq(aliceSeq + 1), queued); - checkMetrics(__LINE__, env, 2, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 2, limit * 2, limit + 1, limit); env.close(); ++limit; - checkMetrics(__LINE__, env, 0, limit * 2, 2, limit); + checkMetrics(*this, env, 0, limit * 2, 2, limit); // But once we close the ledger, we find alice // still has most of her balance, because the @@ -2394,7 +2270,7 @@ public: ////////////////////////////////////////// // Small XRP payment allows later txs fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 0, limit * 2, limit + 1, limit); aliceSeq = env.seq(alice); aliceBal = env.balance(alice); @@ -2405,11 +2281,11 @@ public: // And later transactions are just fine env(noop(alice), seq(aliceSeq + 1), queued); - checkMetrics(__LINE__, env, 2, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 2, limit * 2, limit + 1, limit); env.close(); ++limit; - checkMetrics(__LINE__, env, 0, limit * 2, 2, limit); + checkMetrics(*this, env, 0, limit * 2, 2, limit); // The payment succeeds env.require( @@ -2420,19 +2296,19 @@ public: auto const amount = USD(500000); env(trust(alice, USD(50000000))); env(trust(charlie, USD(50000000))); - checkMetrics(__LINE__, env, 0, limit * 2, 4, limit); + checkMetrics(*this, env, 0, limit * 2, 4, limit); // Close so we don't have to deal // with tx ordering in consensus. env.close(); env(pay(gw, alice, amount)); - checkMetrics(__LINE__, env, 0, limit * 2, 1, limit); + checkMetrics(*this, env, 0, limit * 2, 1, limit); // Close so we don't have to deal // with tx ordering in consensus. env.close(); fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 0, limit * 2, limit + 1, limit); aliceSeq = env.seq(alice); aliceBal = env.balance(alice); @@ -2445,11 +2321,11 @@ public: // But that's fine, because it doesn't affect // alice's XRP balance (other than the fee, of course). env(noop(alice), seq(aliceSeq + 1), queued); - checkMetrics(__LINE__, env, 2, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 2, limit * 2, limit + 1, limit); env.close(); ++limit; - checkMetrics(__LINE__, env, 0, limit * 2, 2, limit); + checkMetrics(*this, env, 0, limit * 2, 2, limit); // So once we close the ledger, alice has her // XRP balance, but her USD balance went to charlie. @@ -2469,7 +2345,7 @@ public: env.close(); fillQueue(env, charlie); - checkMetrics(__LINE__, env, 0, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 0, limit * 2, limit + 1, limit); aliceSeq = env.seq(alice); aliceBal = env.balance(alice); @@ -2485,11 +2361,11 @@ public: // But because the reserve is protected, another // transaction will be allowed to queue env(noop(alice), seq(aliceSeq + 1), queued); - checkMetrics(__LINE__, env, 2, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 2, limit * 2, limit + 1, limit); env.close(); ++limit; - checkMetrics(__LINE__, env, 0, limit * 2, 2, limit); + checkMetrics(*this, env, 0, limit * 2, 2, limit); // So once we close the ledger, alice sent a payment // to charlie using only a portion of her XRP balance @@ -2504,7 +2380,7 @@ public: // Small XRP to IOU payment allows later txs. fillQueue(env, charlie); - checkMetrics(__LINE__, env, 0, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 0, limit * 2, limit + 1, limit); aliceSeq = env.seq(alice); aliceBal = env.balance(alice); @@ -2519,11 +2395,11 @@ public: // And later transactions are just fine env(noop(alice), seq(aliceSeq + 1), queued); - checkMetrics(__LINE__, env, 2, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 2, limit * 2, limit + 1, limit); env.close(); ++limit; - checkMetrics(__LINE__, env, 0, limit * 2, 2, limit); + checkMetrics(*this, env, 0, limit * 2, 2, limit); // So once we close the ledger, alice sent a payment // to charlie using only a portion of her XRP balance @@ -2540,7 +2416,7 @@ public: env.close(); fillQueue(env, charlie); - checkMetrics(__LINE__, env, 0, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 0, limit * 2, limit + 1, limit); aliceSeq = env.seq(alice); aliceBal = env.balance(alice); @@ -2550,11 +2426,11 @@ public: env(noop(alice), seq(aliceSeq + 1), ter(terINSUF_FEE_B)); BEAST_EXPECT(env.balance(alice) == drops(30)); - checkMetrics(__LINE__, env, 1, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 1, limit * 2, limit + 1, limit); env.close(); ++limit; - checkMetrics(__LINE__, env, 0, limit * 2, 1, limit); + checkMetrics(*this, env, 0, limit * 2, 1, limit); BEAST_EXPECT(env.balance(alice) == drops(5)); } @@ -2639,27 +2515,27 @@ public: Env env(*this, makeConfig({{"minimum_txn_in_ledger_standalone", "3"}})); auto const baseFee = env.current()->fees().base.drops(); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 3); + checkMetrics(*this, env, 0, std::nullopt, 0, 3); // Fund accounts while the fee is cheap so they all apply. env.fund(XRP(50000), noripple(alice, bob, charlie)); - checkMetrics(__LINE__, env, 0, std::nullopt, 3, 3); + checkMetrics(*this, env, 0, std::nullopt, 3, 3); // Alice - no fee change yet env(noop(alice)); - checkMetrics(__LINE__, env, 0, std::nullopt, 4, 3); + checkMetrics(*this, env, 0, std::nullopt, 4, 3); // Bob with really high fee - applies env(noop(bob), fee(openLedgerCost(env))); - checkMetrics(__LINE__, env, 0, std::nullopt, 5, 3); + checkMetrics(*this, env, 0, std::nullopt, 5, 3); // Charlie with low fee: queued env(noop(charlie), fee(baseFee * 100), queued); - checkMetrics(__LINE__, env, 1, std::nullopt, 5, 3); + checkMetrics(*this, env, 1, std::nullopt, 5, 3); env.close(); // Verify that the queued transaction was applied - checkMetrics(__LINE__, env, 0, 10, 1, 5); + checkMetrics(*this, env, 0, 10, 1, 5); ///////////////////////////////////////////////////////////////// @@ -2670,7 +2546,7 @@ public: env(noop(bob), fee(baseFee * 100)); env(noop(bob), fee(baseFee * 100)); env(noop(bob), fee(baseFee * 100)); - checkMetrics(__LINE__, env, 0, 10, 6, 5); + checkMetrics(*this, env, 0, 10, 6, 5); // Use explicit fees so we can control which txn // will get dropped @@ -2695,7 +2571,7 @@ public: env(noop(alice), fee(baseFee * 2.1), seq(aliceSeq++), queued); // Queue is full now. - checkMetrics(__LINE__, env, 10, 10, 6, 5, expectedFeeLevel + 1); + checkMetrics(*this, env, 10, 10, 6, 5, expectedFeeLevel + 1); // Try to add another transaction with the default (low) fee, // it should fail because the queue is full. @@ -2825,7 +2701,7 @@ public: auto const bob = Account("bob"); env.fund(XRP(500000), noripple(alice, bob)); - checkMetrics(__LINE__, env, 0, std::nullopt, 2, 1); + checkMetrics(*this, env, 0, std::nullopt, 2, 1); auto const aliceSeq = env.seq(alice); BEAST_EXPECT(env.current()->info().seq == 3); @@ -2845,7 +2721,7 @@ public: seq(aliceSeq + 3), json(R"({"LastLedgerSequence":11})"), ter(terQUEUED)); - checkMetrics(__LINE__, env, 4, std::nullopt, 2, 1); + checkMetrics(*this, env, 4, std::nullopt, 2, 1); auto const bobSeq = env.seq(bob); // Ledger 4 gets 3, // Ledger 5 gets 4, @@ -2854,17 +2730,17 @@ public: { env(noop(bob), seq(bobSeq + i), fee(baseFee * 20), ter(terQUEUED)); } - checkMetrics(__LINE__, env, 4 + 3 + 4 + 5, std::nullopt, 2, 1); + checkMetrics(*this, env, 4 + 3 + 4 + 5, std::nullopt, 2, 1); // Close ledger 3 env.close(); - checkMetrics(__LINE__, env, 4 + 4 + 5, 20, 3, 2); + checkMetrics(*this, env, 4 + 4 + 5, 20, 3, 2); // Close ledger 4 env.close(); - checkMetrics(__LINE__, env, 4 + 5, 30, 4, 3); + checkMetrics(*this, env, 4 + 5, 30, 4, 3); // Close ledger 5 env.close(); // Alice's first two txs expired. - checkMetrics(__LINE__, env, 2, 40, 5, 4); + checkMetrics(*this, env, 2, 40, 5, 4); // Because aliceSeq is missing, aliceSeq + 1 fails env(noop(alice), seq(aliceSeq + 1), ter(terPRE_SEQ)); @@ -2873,27 +2749,27 @@ public: env(fset(alice, asfAccountTxnID), seq(aliceSeq), ter(telCAN_NOT_QUEUE_BLOCKS)); - checkMetrics(__LINE__, env, 2, 40, 5, 4); + checkMetrics(*this, env, 2, 40, 5, 4); // However we can fill the gap with a non-blocker. env(noop(alice), seq(aliceSeq), fee(baseFee * 2), ter(terQUEUED)); - checkMetrics(__LINE__, env, 3, 40, 5, 4); + checkMetrics(*this, env, 3, 40, 5, 4); // Attempt to queue up a new aliceSeq + 1 tx that's a blocker. env(fset(alice, asfAccountTxnID), seq(aliceSeq + 1), ter(telCAN_NOT_QUEUE_BLOCKS)); - checkMetrics(__LINE__, env, 3, 40, 5, 4); + checkMetrics(*this, env, 3, 40, 5, 4); // Queue up a non-blocker replacement for aliceSeq + 1. env(noop(alice), seq(aliceSeq + 1), fee(baseFee * 2), ter(terQUEUED)); - checkMetrics(__LINE__, env, 4, 40, 5, 4); + checkMetrics(*this, env, 4, 40, 5, 4); // Close ledger 6 env.close(); // We expect that all of alice's queued tx's got into // the open ledger. - checkMetrics(__LINE__, env, 0, 50, 4, 5); + checkMetrics(*this, env, 0, 50, 4, 5); BEAST_EXPECT(env.seq(alice) == aliceSeq + 4); } @@ -2927,7 +2803,7 @@ public: auto const bob = Account("bob"); env.fund(XRP(500000), noripple(alice, bob)); - checkMetrics(__LINE__, env, 0, std::nullopt, 2, 1); + checkMetrics(*this, env, 0, std::nullopt, 2, 1); auto const aliceSeq = env.seq(alice); BEAST_EXPECT(env.current()->info().seq == 3); @@ -2974,7 +2850,7 @@ public: seq(aliceSeq + 19), json(R"({"LastLedgerSequence":11})"), ter(terQUEUED)); - checkMetrics(__LINE__, env, 10, std::nullopt, 2, 1); + checkMetrics(*this, env, 10, std::nullopt, 2, 1); auto const bobSeq = env.seq(bob); // Ledger 4 gets 2 from bob and 1 from alice, @@ -2984,21 +2860,21 @@ public: { env(noop(bob), seq(bobSeq + i), fee(baseFee * 20), ter(terQUEUED)); } - checkMetrics(__LINE__, env, 10 + 2 + 4 + 5, std::nullopt, 2, 1); + checkMetrics(*this, env, 10 + 2 + 4 + 5, std::nullopt, 2, 1); // Close ledger 3 env.close(); - checkMetrics(__LINE__, env, 9 + 4 + 5, 20, 3, 2); + checkMetrics(*this, env, 9 + 4 + 5, 20, 3, 2); BEAST_EXPECT(env.seq(alice) == aliceSeq + 12); // Close ledger 4 env.close(); - checkMetrics(__LINE__, env, 9 + 5, 30, 4, 3); + checkMetrics(*this, env, 9 + 5, 30, 4, 3); BEAST_EXPECT(env.seq(alice) == aliceSeq + 12); // Close ledger 5 env.close(); // Three of Alice's txs expired. - checkMetrics(__LINE__, env, 6, 40, 5, 4); + checkMetrics(*this, env, 6, 40, 5, 4); BEAST_EXPECT(env.seq(alice) == aliceSeq + 12); // Top off Alice's queue again using Tickets so the sequence gap is @@ -3009,7 +2885,7 @@ public: env(noop(alice), ticket::use(aliceSeq + 4), ter(terQUEUED)); env(noop(alice), ticket::use(aliceSeq + 5), ter(terQUEUED)); env(noop(alice), ticket::use(aliceSeq + 6), ter(telCAN_NOT_QUEUE_FULL)); - checkMetrics(__LINE__, env, 11, 40, 5, 4); + checkMetrics(*this, env, 11, 40, 5, 4); // Even though alice's queue is full we can still slide in a couple // more transactions because she has a sequence gap. But we @@ -3040,7 +2916,7 @@ public: // Finally we can fill in the entire gap. env(noop(alice), seq(aliceSeq + 18), ter(terQUEUED)); - checkMetrics(__LINE__, env, 14, 40, 5, 4); + checkMetrics(*this, env, 14, 40, 5, 4); // Verify that nothing can be added now that the gap is filled. env(noop(alice), seq(aliceSeq + 20), ter(telCAN_NOT_QUEUE_FULL)); @@ -3049,18 +2925,18 @@ public: // but alice adds some more transaction(s) so expectedCount // may not reduce to 8. env.close(); - checkMetrics(__LINE__, env, 9, 50, 6, 5); + checkMetrics(*this, env, 9, 50, 6, 5); BEAST_EXPECT(env.seq(alice) == aliceSeq + 15); // Close ledger 7. That should remove 4 more of alice's transactions. env.close(); - checkMetrics(__LINE__, env, 2, 60, 7, 6); + checkMetrics(*this, env, 2, 60, 7, 6); BEAST_EXPECT(env.seq(alice) == aliceSeq + 19); // Close one last ledger to see all of alice's transactions moved // into the ledger, including the tickets env.close(); - checkMetrics(__LINE__, env, 0, 70, 2, 7); + checkMetrics(*this, env, 0, 70, 2, 7); BEAST_EXPECT(env.seq(alice) == aliceSeq + 21); } @@ -3079,7 +2955,7 @@ public: env.fund(XRP(100000), alice, bob); fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, std::nullopt, 7, 6); + checkMetrics(*this, env, 0, std::nullopt, 7, 6); // Queue up several transactions for alice sign-and-submit auto const aliceSeq = env.seq(alice); @@ -3100,7 +2976,7 @@ public: noop(alice), fee(baseFee * 100), seq(none), ter(terQUEUED))( submitParams); } - checkMetrics(__LINE__, env, 5, std::nullopt, 7, 6); + checkMetrics(*this, env, 5, std::nullopt, 7, 6); { auto aliceStat = txQ.getAccountTxs(alice.id()); SeqProxy seq = SeqProxy::sequence(aliceSeq); @@ -3126,25 +3002,25 @@ public: // Give them a higher fee so they'll beat alice's. for (int i = 0; i < 8; ++i) envs(noop(bob), fee(baseFee * 200), seq(none), ter(terQUEUED))(); - checkMetrics(__LINE__, env, 13, std::nullopt, 7, 6); + checkMetrics(*this, env, 13, std::nullopt, 7, 6); env.close(); - checkMetrics(__LINE__, env, 5, 14, 8, 7); + checkMetrics(*this, env, 5, 14, 8, 7); // Put some more txs in the queue for bob. // Give them a higher fee so they'll beat alice's. fillQueue(env, bob); for (int i = 0; i < 9; ++i) envs(noop(bob), fee(baseFee * 200), seq(none), ter(terQUEUED))(); - checkMetrics(__LINE__, env, 14, 14, 8, 7, 25601); + checkMetrics(*this, env, 14, 14, 8, 7, 25601); env.close(); // Put some more txs in the queue for bob. // Give them a higher fee so they'll beat alice's. fillQueue(env, bob); for (int i = 0; i < 10; ++i) envs(noop(bob), fee(baseFee * 200), seq(none), ter(terQUEUED))(); - checkMetrics(__LINE__, env, 15, 16, 9, 8); + checkMetrics(*this, env, 15, 16, 9, 8); env.close(); - checkMetrics(__LINE__, env, 4, 18, 10, 9); + checkMetrics(*this, env, 4, 18, 10, 9); { // Bob has nothing left in the queue. auto bobStat = txQ.getAccountTxs(bob.id()); @@ -3172,7 +3048,7 @@ public: // Now, fill the gap. envs(noop(alice), fee(baseFee * 100), seq(none), ter(terQUEUED))( submitParams); - checkMetrics(__LINE__, env, 5, 18, 10, 9); + checkMetrics(*this, env, 5, 18, 10, 9); { auto aliceStat = txQ.getAccountTxs(alice.id()); auto seq = aliceSeq; @@ -3187,7 +3063,7 @@ public: } env.close(); - checkMetrics(__LINE__, env, 0, 20, 5, 10); + checkMetrics(*this, env, 0, 20, 5, 10); { // Bob's data has been cleaned up. auto bobStat = txQ.getAccountTxs(bob.id()); @@ -3246,10 +3122,10 @@ public: BEAST_EXPECT(!queue_data.isMember(jss::max_spend_drops_total)); BEAST_EXPECT(!queue_data.isMember(jss::transactions)); } - checkMetrics(__LINE__, env, 0, 6, 0, 3); + checkMetrics(*this, env, 0, 6, 0, 3); fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, 6, 4, 3); + checkMetrics(*this, env, 0, 6, 4, 3); { auto const info = env.rpc("json", "account_info", withQueue); @@ -3278,7 +3154,7 @@ public: submitParams); envs(noop(alice), fee(baseFee * 10), seq(none), ter(terQUEUED))( submitParams); - checkMetrics(__LINE__, env, 4, 6, 4, 3); + checkMetrics(*this, env, 4, 6, 4, 3); { auto const info = env.rpc("json", "account_info", withQueue); @@ -3330,7 +3206,7 @@ public: // Drain the queue so we can queue up a blocker. env.close(); - checkMetrics(__LINE__, env, 0, 8, 4, 4); + checkMetrics(*this, env, 0, 8, 4, 4); // Fill the ledger and then queue up a blocker. envs(noop(alice), seq(none))(submitParams); @@ -3341,7 +3217,7 @@ public: seq(none), json(jss::LastLedgerSequence, 10), ter(terQUEUED))(submitParams); - checkMetrics(__LINE__, env, 1, 8, 5, 4); + checkMetrics(*this, env, 1, 8, 5, 4); { auto const info = env.rpc("json", "account_info", withQueue); @@ -3405,7 +3281,7 @@ public: fee(baseFee * 10), seq(none), ter(telCAN_NOT_QUEUE_BLOCKED))(submitParams); - checkMetrics(__LINE__, env, 1, 8, 5, 4); + checkMetrics(*this, env, 1, 8, 5, 4); { auto const info = env.rpc("json", "account_info", withQueue); @@ -3483,9 +3359,9 @@ public: } env.close(); - checkMetrics(__LINE__, env, 0, 10, 2, 5); + checkMetrics(*this, env, 0, 10, 2, 5); env.close(); - checkMetrics(__LINE__, env, 0, 10, 0, 5); + checkMetrics(*this, env, 0, 10, 0, 5); { auto const info = env.rpc("json", "account_info", withQueue); @@ -3555,10 +3431,10 @@ public: state[jss::load_factor_fee_reference] == 256); } - checkMetrics(__LINE__, env, 0, 6, 0, 3); + checkMetrics(*this, env, 0, 6, 0, 3); fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, 6, 4, 3); + checkMetrics(*this, env, 0, 6, 4, 3); auto aliceSeq = env.seq(alice); auto submitParams = Json::Value(Json::objectValue); @@ -3568,7 +3444,7 @@ public: fee(baseFee * 10), seq(aliceSeq + i), ter(terQUEUED))(submitParams); - checkMetrics(__LINE__, env, 4, 6, 4, 3); + checkMetrics(*this, env, 4, 6, 4, 3); { auto const server_info = env.rpc("server_info"); @@ -3794,7 +3670,7 @@ public: // Fund the first few accounts at non escalated fee env.fund(XRP(50000), noripple(a, b, c, d)); - checkMetrics(__LINE__, env, 0, std::nullopt, 4, 3); + checkMetrics(*this, env, 0, std::nullopt, 4, 3); // First transaction establishes the messaging using namespace std::chrono_literals; @@ -3844,7 +3720,7 @@ public: jv[jss::load_factor_fee_reference] == 256; })); - checkMetrics(__LINE__, env, 0, 8, 0, 4); + checkMetrics(*this, env, 0, 8, 0, 4); // Fund then next few accounts at non escalated fee env.fund(XRP(50000), noripple(e, f, g, h, i)); @@ -3858,7 +3734,7 @@ public: env(noop(e), fee(baseFee), queued); env(noop(f), fee(baseFee), queued); env(noop(g), fee(baseFee), queued); - checkMetrics(__LINE__, env, 7, 8, 5, 4); + checkMetrics(*this, env, 7, 8, 5, 4); // Last transaction escalates the fee BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) { @@ -3928,7 +3804,7 @@ public: auto alice = Account("alice"); auto bob = Account("bob"); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 3); + checkMetrics(*this, env, 0, std::nullopt, 0, 3); env.fund(XRP(50000000), alice, bob); fillQueue(env, alice); @@ -3982,7 +3858,7 @@ public: seq(aliceSeq++), ter(terQUEUED)); - checkMetrics(__LINE__, env, 3, std::nullopt, 4, 3); + checkMetrics(*this, env, 3, std::nullopt, 4, 3); // Figure out how much it would cost to cover all the // queued txs + itself @@ -3994,7 +3870,7 @@ public: // the edge case test. env(noop(alice), fee(totalFee), seq(aliceSeq++), ter(terQUEUED)); - checkMetrics(__LINE__, env, 4, std::nullopt, 4, 3); + checkMetrics(*this, env, 4, std::nullopt, 4, 3); // Now repeat the process including the new tx // and avoiding the rounding error @@ -4004,7 +3880,7 @@ public: // Submit a transaction with that fee. It will succeed. env(noop(alice), fee(totalFee), seq(aliceSeq++)); - checkMetrics(__LINE__, env, 0, std::nullopt, 9, 3); + checkMetrics(*this, env, 0, std::nullopt, 9, 3); } testcase("replace last tx with enough to clear queue"); @@ -4029,7 +3905,7 @@ public: seq(aliceSeq++), ter(terQUEUED)); - checkMetrics(__LINE__, env, 3, std::nullopt, 9, 3); + checkMetrics(*this, env, 3, std::nullopt, 9, 3); // Figure out how much it would cost to cover all the // queued txs + itself @@ -4041,10 +3917,10 @@ public: env(noop(alice), fee(totalFee), seq(aliceSeq++)); // The queue is clear - checkMetrics(__LINE__, env, 0, std::nullopt, 12, 3); + checkMetrics(*this, env, 0, std::nullopt, 12, 3); env.close(); - checkMetrics(__LINE__, env, 0, 24, 0, 12); + checkMetrics(*this, env, 0, 24, 0, 12); } testcase("replace middle tx with enough to clear queue"); @@ -4060,7 +3936,7 @@ public: ter(terQUEUED)); } - checkMetrics(__LINE__, env, 5, 24, 13, 12); + checkMetrics(*this, env, 5, 24, 13, 12); // Figure out how much it would cost to cover 3 txns uint64_t const totalFee = calcTotalFee(baseFee * 10 * 2, 3); @@ -4068,7 +3944,7 @@ public: aliceSeq -= 3; env(noop(alice), fee(totalFee), seq(aliceSeq++)); - checkMetrics(__LINE__, env, 2, 24, 16, 12); + checkMetrics(*this, env, 2, 24, 16, 12); auto const aliceQueue = env.app().getTxQ().getAccountTxs(alice.id()); BEAST_EXPECT(aliceQueue.size() == 2); @@ -4083,7 +3959,7 @@ public: // Close the ledger to clear the queue env.close(); - checkMetrics(__LINE__, env, 0, 32, 2, 16); + checkMetrics(*this, env, 0, 32, 2, 16); } testcase("clear queue failure (load)"); @@ -4109,7 +3985,7 @@ public: totalPaid += baseFee * 2.2; } - checkMetrics(__LINE__, env, 4, 32, 17, 16); + checkMetrics(*this, env, 4, 32, 17, 16); // Figure out how much it would cost to cover all the txns // + 1 @@ -4123,11 +3999,11 @@ public: env(noop(alice), fee(totalFee), seq(aliceSeq++), ter(terQUEUED)); // The original last transaction is still in the queue - checkMetrics(__LINE__, env, 5, 32, 17, 16); + checkMetrics(*this, env, 5, 32, 17, 16); // With high load, some of the txs stay in the queue env.close(); - checkMetrics(__LINE__, env, 3, 34, 2, 17); + checkMetrics(*this, env, 3, 34, 2, 17); // Load drops back down feeTrack.setRemoteFee(origFee); @@ -4135,14 +4011,14 @@ public: // Because of the earlier failure, alice can not clear the queue, // no matter how high the fee fillQueue(env, bob); - checkMetrics(__LINE__, env, 3, 34, 18, 17); + checkMetrics(*this, env, 3, 34, 18, 17); env(noop(alice), fee(XRP(1)), seq(aliceSeq++), ter(terQUEUED)); - checkMetrics(__LINE__, env, 4, 34, 18, 17); + checkMetrics(*this, env, 4, 34, 18, 17); // With normal load, those txs get into the ledger env.close(); - checkMetrics(__LINE__, env, 0, 36, 4, 18); + checkMetrics(*this, env, 0, 36, 4, 18); } } @@ -4164,77 +4040,77 @@ public: {"maximum_txn_per_account", "200"}})); auto alice = Account("alice"); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 3); + checkMetrics(*this, env, 0, std::nullopt, 0, 3); env.fund(XRP(50000000), alice); fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, std::nullopt, 4, 3); + checkMetrics(*this, env, 0, std::nullopt, 4, 3); auto seqAlice = env.seq(alice); auto txCount = 140; for (int i = 0; i < txCount; ++i) env(noop(alice), seq(seqAlice++), ter(terQUEUED)); - checkMetrics(__LINE__, env, txCount, std::nullopt, 4, 3); + checkMetrics(*this, env, txCount, std::nullopt, 4, 3); // Close a few ledgers successfully, so the limit grows env.close(); // 4 + 25% = 5 txCount -= 6; - checkMetrics(__LINE__, env, txCount, 10, 6, 5, 257); + checkMetrics(*this, env, txCount, 10, 6, 5, 257); env.close(); // 6 + 25% = 7 txCount -= 8; - checkMetrics(__LINE__, env, txCount, 14, 8, 7, 257); + checkMetrics(*this, env, txCount, 14, 8, 7, 257); env.close(); // 8 + 25% = 10 txCount -= 11; - checkMetrics(__LINE__, env, txCount, 20, 11, 10, 257); + checkMetrics(*this, env, txCount, 20, 11, 10, 257); env.close(); // 11 + 25% = 13 txCount -= 14; - checkMetrics(__LINE__, env, txCount, 26, 14, 13, 257); + checkMetrics(*this, env, txCount, 26, 14, 13, 257); env.close(); // 14 + 25% = 17 txCount -= 18; - checkMetrics(__LINE__, env, txCount, 34, 18, 17, 257); + checkMetrics(*this, env, txCount, 34, 18, 17, 257); env.close(); // 18 + 25% = 22 txCount -= 23; - checkMetrics(__LINE__, env, txCount, 44, 23, 22, 257); + checkMetrics(*this, env, txCount, 44, 23, 22, 257); env.close(); // 23 + 25% = 28 txCount -= 29; - checkMetrics(__LINE__, env, txCount, 56, 29, 28); + checkMetrics(*this, env, txCount, 56, 29, 28); // From 3 expected to 28 in 7 "fast" ledgers. // Close the ledger with a delay. env.close(env.now() + 5s, 10000ms); txCount -= 15; - checkMetrics(__LINE__, env, txCount, 56, 15, 14); + checkMetrics(*this, env, txCount, 56, 15, 14); // Close the ledger with a delay. env.close(env.now() + 5s, 10000ms); txCount -= 8; - checkMetrics(__LINE__, env, txCount, 56, 8, 7); + checkMetrics(*this, env, txCount, 56, 8, 7); // Close the ledger with a delay. env.close(env.now() + 5s, 10000ms); txCount -= 4; - checkMetrics(__LINE__, env, txCount, 56, 4, 3); + checkMetrics(*this, env, txCount, 56, 4, 3); // From 28 expected back down to 3 in 3 "slow" ledgers. // Confirm the minimum sticks env.close(env.now() + 5s, 10000ms); txCount -= 4; - checkMetrics(__LINE__, env, txCount, 56, 4, 3); + checkMetrics(*this, env, txCount, 56, 4, 3); BEAST_EXPECT(!txCount); } @@ -4250,35 +4126,35 @@ public: {"maximum_txn_per_account", "200"}})); auto alice = Account("alice"); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 3); + checkMetrics(*this, env, 0, std::nullopt, 0, 3); env.fund(XRP(50000000), alice); fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, std::nullopt, 4, 3); + checkMetrics(*this, env, 0, std::nullopt, 4, 3); auto seqAlice = env.seq(alice); auto txCount = 43; for (int i = 0; i < txCount; ++i) env(noop(alice), seq(seqAlice++), ter(terQUEUED)); - checkMetrics(__LINE__, env, txCount, std::nullopt, 4, 3); + checkMetrics(*this, env, txCount, std::nullopt, 4, 3); // Close a few ledgers successfully, so the limit grows env.close(); // 4 + 150% = 10 txCount -= 11; - checkMetrics(__LINE__, env, txCount, 20, 11, 10, 257); + checkMetrics(*this, env, txCount, 20, 11, 10, 257); env.close(); // 11 + 150% = 27 txCount -= 28; - checkMetrics(__LINE__, env, txCount, 54, 28, 27); + checkMetrics(*this, env, txCount, 54, 28, 27); // From 3 expected to 28 in 7 "fast" ledgers. // Close the ledger with a delay. env.close(env.now() + 5s, 10000ms); txCount -= 4; - checkMetrics(__LINE__, env, txCount, 54, 4, 3); + checkMetrics(*this, env, txCount, 54, 4, 3); // From 28 expected back down to 3 in 3 "slow" ledgers. @@ -4306,19 +4182,19 @@ public: auto const queued = ter(terQUEUED); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 3); + checkMetrics(*this, env, 0, std::nullopt, 0, 3); // Create account env.fund(XRP(50000), noripple(alice)); - checkMetrics(__LINE__, env, 0, std::nullopt, 1, 3); + checkMetrics(*this, env, 0, std::nullopt, 1, 3); fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, std::nullopt, 4, 3); + checkMetrics(*this, env, 0, std::nullopt, 4, 3); // Queue a transaction auto const aliceSeq = env.seq(alice); env(noop(alice), queued); - checkMetrics(__LINE__, env, 1, std::nullopt, 4, 3); + checkMetrics(*this, env, 1, std::nullopt, 4, 3); // Now, apply a (different) transaction directly // to the open ledger, bypassing the queue @@ -4334,23 +4210,23 @@ public: return result.applied; }); // the queued transaction is still there - checkMetrics(__LINE__, env, 1, std::nullopt, 5, 3); + checkMetrics(*this, env, 1, std::nullopt, 5, 3); // The next transaction should be able to go into the open // ledger, even though aliceSeq is queued. In earlier incarnations // of the TxQ this would cause an assert. env(noop(alice), seq(aliceSeq + 1), fee(openLedgerCost(env))); - checkMetrics(__LINE__, env, 1, std::nullopt, 6, 3); + checkMetrics(*this, env, 1, std::nullopt, 6, 3); // Now queue a couple more transactions to make sure // they succeed despite aliceSeq being queued env(noop(alice), seq(aliceSeq + 2), queued); env(noop(alice), seq(aliceSeq + 3), queued); - checkMetrics(__LINE__, env, 3, std::nullopt, 6, 3); + checkMetrics(*this, env, 3, std::nullopt, 6, 3); // Now close the ledger. One of the queued transactions // (aliceSeq) should be dropped. env.close(); - checkMetrics(__LINE__, env, 0, 12, 2, 6); + checkMetrics(*this, env, 0, 12, 2, 6); } void @@ -4371,11 +4247,11 @@ public: auto queued = ter(terQUEUED); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 3); + checkMetrics(*this, env, 0, std::nullopt, 0, 3); // Create account env.fund(XRP(50000), noripple(alice)); - checkMetrics(__LINE__, env, 0, std::nullopt, 1, 3); + checkMetrics(*this, env, 0, std::nullopt, 1, 3); // Create tickets std::uint32_t const tktSeq0{env.seq(alice) + 1}; @@ -4383,12 +4259,12 @@ public: // Fill the queue so the next transaction will be queued. fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, std::nullopt, 4, 3); + checkMetrics(*this, env, 0, std::nullopt, 4, 3); // Queue a transaction with a ticket. Leave an unused ticket // on either side. env(noop(alice), ticket::use(tktSeq0 + 1), queued); - checkMetrics(__LINE__, env, 1, std::nullopt, 4, 3); + checkMetrics(*this, env, 1, std::nullopt, 4, 3); // Now, apply a (different) transaction directly // to the open ledger, bypassing the queue @@ -4406,25 +4282,25 @@ public: return result.applied; }); // the queued transaction is still there - checkMetrics(__LINE__, env, 1, std::nullopt, 5, 3); + checkMetrics(*this, env, 1, std::nullopt, 5, 3); // The next (sequence-based) transaction should be able to go into // the open ledger, even though tktSeq0 is queued. Note that this // sequence-based transaction goes in front of the queued // transaction, so the queued transaction is left in the queue. env(noop(alice), fee(openLedgerCost(env))); - checkMetrics(__LINE__, env, 1, std::nullopt, 6, 3); + checkMetrics(*this, env, 1, std::nullopt, 6, 3); // We should be able to do the same thing with a ticket that goes // if front of the queued transaction. This one too will leave // the queued transaction in place. env(noop(alice), ticket::use(tktSeq0 + 0), fee(openLedgerCost(env))); - checkMetrics(__LINE__, env, 1, std::nullopt, 7, 3); + checkMetrics(*this, env, 1, std::nullopt, 7, 3); // We have one ticketed transaction in the queue. We should able // to add another to the queue. env(noop(alice), ticket::use(tktSeq0 + 2), queued); - checkMetrics(__LINE__, env, 2, std::nullopt, 7, 3); + checkMetrics(*this, env, 2, std::nullopt, 7, 3); // Here we try to force the queued transactions into the ledger by // adding one more queued (ticketed) transaction that pays enough @@ -4440,12 +4316,12 @@ public: // transaction is equally capable of going into the ledger independent // of all other ticket- or sequence-based transactions. env(noop(alice), ticket::use(tktSeq0 + 3), fee(XRP(10))); - checkMetrics(__LINE__, env, 2, std::nullopt, 8, 3); + checkMetrics(*this, env, 2, std::nullopt, 8, 3); // Now close the ledger. One of the queued transactions // (the one with tktSeq0 + 1) should be dropped. env.close(); - checkMetrics(__LINE__, env, 0, 16, 1, 8); + checkMetrics(*this, env, 0, 16, 1, 8); } void @@ -4496,7 +4372,7 @@ public: env.close(); env.fund(XRP(10000), fiona); env.close(); - checkMetrics(__LINE__, env, 0, 10, 0, 2); + checkMetrics(*this, env, 0, 10, 0, 2); // Close ledgers until the amendments show up. int i = 0; @@ -4508,7 +4384,7 @@ public: } auto expectedPerLedger = ripple::detail::numUpVotedAmendments() + 1; checkMetrics( - __LINE__, env, 0, 5 * expectedPerLedger, 0, expectedPerLedger); + *this, env, 0, 5 * expectedPerLedger, 0, expectedPerLedger); // Now wait 2 weeks modulo 256 ledgers for the amendments to be // enabled. Speed the process by closing ledgers every 80 minutes, @@ -4524,7 +4400,7 @@ public: // We're very close to the flag ledger. Fill the ledger. fillQueue(env, alice); checkMetrics( - __LINE__, + *this, env, 0, 5 * expectedPerLedger, @@ -4575,7 +4451,7 @@ public: } std::size_t expectedInQueue = 60; checkMetrics( - __LINE__, + *this, env, expectedInQueue, 5 * expectedPerLedger, @@ -4602,7 +4478,7 @@ public: expectedInLedger -= expectedInQueue; ++expectedPerLedger; checkMetrics( - __LINE__, + *this, env, expectedInQueue, 5 * expectedPerLedger, @@ -4689,7 +4565,7 @@ public: // of their transactions expire out of the queue. To start out // alice fills the ledger. fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, 50, 7, 6); + checkMetrics(*this, env, 0, 50, 7, 6); // Now put a few transactions into alice's queue, including one that // will expire out soon. @@ -4735,9 +4611,9 @@ public: env(noop(fiona), seq(seqFiona++), fee(--feeDrops), ter(terQUEUED)); } - checkMetrics(__LINE__, env, 34, 50, 7, 6); + checkMetrics(*this, env, 34, 50, 7, 6); env.close(); - checkMetrics(__LINE__, env, 26, 50, 8, 7); + checkMetrics(*this, env, 26, 50, 8, 7); // Re-fill the queue so alice and bob stay stuck. feeDrops = medFee; @@ -4748,9 +4624,9 @@ public: env(noop(ellie), seq(seqEllie++), fee(--feeDrops), ter(terQUEUED)); env(noop(fiona), seq(seqFiona++), fee(--feeDrops), ter(terQUEUED)); } - checkMetrics(__LINE__, env, 38, 50, 8, 7); + checkMetrics(*this, env, 38, 50, 8, 7); env.close(); - checkMetrics(__LINE__, env, 29, 50, 9, 8); + checkMetrics(*this, env, 29, 50, 9, 8); // One more time... feeDrops = medFee; @@ -4761,9 +4637,9 @@ public: env(noop(ellie), seq(seqEllie++), fee(--feeDrops), ter(terQUEUED)); env(noop(fiona), seq(seqFiona++), fee(--feeDrops), ter(terQUEUED)); } - checkMetrics(__LINE__, env, 41, 50, 9, 8); + checkMetrics(*this, env, 41, 50, 9, 8); env.close(); - checkMetrics(__LINE__, env, 29, 50, 10, 9); + checkMetrics(*this, env, 29, 50, 10, 9); // Finally the stage is set. alice's and bob's transactions expired // out of the queue which caused the dropPenalty flag to be set on @@ -4785,7 +4661,7 @@ public: env(noop(carol), seq(seqCarol++), fee(--feeDrops), ter(terQUEUED)); env(noop(daria), seq(seqDaria++), fee(--feeDrops), ter(terQUEUED)); env(noop(ellie), seq(seqEllie++), fee(--feeDrops), ter(terQUEUED)); - checkMetrics(__LINE__, env, 48, 50, 10, 9); + checkMetrics(*this, env, 48, 50, 10, 9); // Now induce a fee jump which should cause all the transactions // in the queue to fail with telINSUF_FEE_P. @@ -4802,7 +4678,7 @@ public: // o The _last_ transaction should be dropped from alice's queue. // o The first failing transaction should be dropped from bob's queue. env.close(); - checkMetrics(__LINE__, env, 46, 50, 0, 10); + checkMetrics(*this, env, 46, 50, 0, 10); // Run the local fee back down. while (env.app().getFeeTrack().lowerLocalFee()) @@ -4810,7 +4686,7 @@ public: // bob fills the ledger so it's easier to probe the TxQ. fillQueue(env, bob); - checkMetrics(__LINE__, env, 46, 50, 11, 10); + checkMetrics(*this, env, 46, 50, 11, 10); // Before the close() alice had two transactions in her queue. // We now expect her to have one. Here's the state of alice's queue. @@ -4928,7 +4804,7 @@ public: env.close(); - checkMetrics(__LINE__, env, 0, 50, 4, 6); + checkMetrics(*this, env, 0, 50, 4, 6); } { @@ -4989,7 +4865,7 @@ public: // The ticket transactions that didn't succeed or get queued succeed // this time because the tickets got consumed when the offers came // out of the queue - checkMetrics(__LINE__, env, 0, 50, 8, 7); + checkMetrics(*this, env, 0, 50, 8, 7); } } @@ -5010,7 +4886,7 @@ public: {"account_reserve", "0"}, {"owner_reserve", "0"}})); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 3); + checkMetrics(*this, env, 0, std::nullopt, 0, 3); // ledgers in queue is 2 because of makeConfig auto const initQueueMax = initFee(env, 3, 2, 0, 0, 0); @@ -5056,34 +4932,34 @@ public: } } - checkMetrics(__LINE__, env, 0, initQueueMax, 0, 3); + checkMetrics(*this, env, 0, initQueueMax, 0, 3); // The noripple is to reduce the number of transactions required to // fund the accounts. There is no rippling in this test. env.fund(XRP(100000), noripple(alice)); - checkMetrics(__LINE__, env, 0, initQueueMax, 1, 3); + checkMetrics(*this, env, 0, initQueueMax, 1, 3); env.close(); - checkMetrics(__LINE__, env, 0, 6, 0, 3); + checkMetrics(*this, env, 0, 6, 0, 3); fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, 6, 4, 3); + checkMetrics(*this, env, 0, 6, 4, 3); env(noop(alice), fee(openLedgerCost(env))); - checkMetrics(__LINE__, env, 0, 6, 5, 3); + checkMetrics(*this, env, 0, 6, 5, 3); auto aliceSeq = env.seq(alice); env(noop(alice), queued); - checkMetrics(__LINE__, env, 1, 6, 5, 3); + checkMetrics(*this, env, 1, 6, 5, 3); env(noop(alice), seq(aliceSeq + 1), fee(10), queued); - checkMetrics(__LINE__, env, 2, 6, 5, 3); + checkMetrics(*this, env, 2, 6, 5, 3); { auto const fee = env.rpc("fee"); @@ -5126,7 +5002,7 @@ public: env.close(); - checkMetrics(__LINE__, env, 0, 10, 2, 5); + checkMetrics(*this, env, 0, 10, 2, 5); } void diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index d6e1dfc73f..22814b3086 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -17,18 +17,8 @@ */ //============================================================================== -#include +#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include @@ -53,10 +43,11 @@ namespace ripple { -using namespace test::jtx; - class Vault_test : public beast::unit_test::suite { + using PrettyAsset = ripple::test::jtx::PrettyAsset; + using PrettyAmount = ripple::test::jtx::PrettyAmount; + static auto constexpr negativeAmount = [](PrettyAsset const& asset) -> PrettyAmount { return {STAmount{asset.raw(), 1ul, 0, true, STAmount::unchecked{}}, ""}; @@ -349,7 +340,7 @@ class Vault_test : public beast::unit_test::suite Account const& owner, Account const& depositor, Account const& charlie)> setup) { - Env env{*this, supported_amendments() | featureSingleAssetVault}; + Env env{*this, testable_amendments() | featureSingleAssetVault}; Account issuer{"issuer"}; Account owner{"owner"}; Account depositor{"depositor"}; @@ -425,7 +416,7 @@ class Vault_test : public beast::unit_test::suite struct CaseArgs { FeatureBitset features = - supported_amendments() | featureSingleAssetVault; + testable_amendments() | featureSingleAssetVault; }; auto testCase = [&, this]( @@ -503,7 +494,7 @@ class Vault_test : public beast::unit_test::suite env(tx, ter{temDISABLED}); } }, - {.features = supported_amendments() - featureSingleAssetVault}); + {.features = testable_amendments() - featureSingleAssetVault}); testCase([&](Env& env, Account const& issuer, @@ -634,7 +625,7 @@ class Vault_test : public beast::unit_test::suite env(tx, ter{temDISABLED}); } }, - {.features = (supported_amendments() | featureSingleAssetVault) - + {.features = (testable_amendments() | featureSingleAssetVault) - featurePermissionedDomains}); testCase([&](Env& env, @@ -952,14 +943,15 @@ class Vault_test : public beast::unit_test::suite { using namespace test::jtx; - auto testCase = [this](std::function test) { - Env env{*this, supported_amendments() | featureSingleAssetVault}; + auto testCase = [this]( + std::function test) { + Env env{*this, testable_amendments() | featureSingleAssetVault}; Account issuer{"issuer"}; Account owner{"owner"}; Account depositor{"depositor"}; @@ -1101,8 +1093,7 @@ class Vault_test : public beast::unit_test::suite { { testcase("IOU fail create frozen"); - Env env{ - *this, supported_amendments() | featureSingleAssetVault}; + Env env{*this, testable_amendments() | featureSingleAssetVault}; Account issuer{"issuer"}; Account owner{"owner"}; env.fund(XRP(1000), issuer, owner); @@ -1121,8 +1112,7 @@ class Vault_test : public beast::unit_test::suite { testcase("IOU fail create no ripling"); - Env env{ - *this, supported_amendments() | featureSingleAssetVault}; + Env env{*this, testable_amendments() | featureSingleAssetVault}; Account issuer{"issuer"}; Account owner{"owner"}; env.fund(XRP(1000), issuer, owner); @@ -1140,8 +1130,7 @@ class Vault_test : public beast::unit_test::suite { testcase("IOU no issuer"); - Env env{ - *this, supported_amendments() | featureSingleAssetVault}; + Env env{*this, testable_amendments() | featureSingleAssetVault}; Account issuer{"issuer"}; Account owner{"owner"}; env.fund(XRP(1000), owner); @@ -1160,7 +1149,7 @@ class Vault_test : public beast::unit_test::suite { testcase("IOU fail create vault for AMM LPToken"); - Env env{*this, supported_amendments() | featureSingleAssetVault}; + Env env{*this, testable_amendments() | featureSingleAssetVault}; Account const gw("gateway"); Account const alice("alice"); Account const carol("carol"); @@ -1210,7 +1199,8 @@ class Vault_test : public beast::unit_test::suite testCreateFailMPT() { using namespace test::jtx; - Env env{*this, supported_amendments() | featureSingleAssetVault}; + + Env env{*this, testable_amendments() | featureSingleAssetVault}; Account issuer{"issuer"}; Account owner{"owner"}; Account depositor{"depositor"}; @@ -1231,7 +1221,8 @@ class Vault_test : public beast::unit_test::suite testNonTransferableShares() { using namespace test::jtx; - Env env{*this, supported_amendments() | featureSingleAssetVault}; + + Env env{*this, testable_amendments() | featureSingleAssetVault}; Account issuer{"issuer"}; Account owner{"owner"}; Account depositor{"depositor"}; @@ -1357,7 +1348,7 @@ class Vault_test : public beast::unit_test::suite Vault& vault, MPTTester& mptt)> test, CaseArgs args = {}) { - Env env{*this, supported_amendments() | featureSingleAssetVault}; + Env env{*this, testable_amendments() | featureSingleAssetVault}; Account issuer{"issuer"}; Account owner{"owner"}; Account depositor{"depositor"}; @@ -1753,7 +1744,7 @@ class Vault_test : public beast::unit_test::suite { testcase("MPT shares to a vault"); - Env env{*this, supported_amendments() | featureSingleAssetVault}; + Env env{*this, testable_amendments() | featureSingleAssetVault}; Account owner{"owner"}; Account issuer{"issuer"}; env.fund(XRP(1000000), owner, issuer); @@ -1787,6 +1778,8 @@ class Vault_test : public beast::unit_test::suite void testWithIOU() { + using namespace test::jtx; + auto testCase = [&, this]( std::function issuanceId, std::function vaultBalance)> test) { - Env env{ - *this, supported_amendments() | featureSingleAssetVault}; + Env env{*this, testable_amendments() | featureSingleAssetVault}; Account const owner{"owner"}; Account const issuer{"issuer"}; Account const charlie{"charlie"}; @@ -2242,9 +2234,11 @@ class Vault_test : public beast::unit_test::suite void testWithDomainCheck() { + using namespace test::jtx; + testcase("private vault"); - Env env{*this, supported_amendments() | featureSingleAssetVault}; + Env env{*this, testable_amendments() | featureSingleAssetVault}; Account issuer{"issuer"}; Account owner{"owner"}; Account depositor{"depositor"}; @@ -2273,6 +2267,8 @@ class Vault_test : public beast::unit_test::suite env(pay(issuer, owner, asset(500))); env.trust(asset(1000), depositor); env(pay(issuer, depositor, asset(500))); + env.trust(asset(1000), charlie); + env(pay(issuer, charlie, asset(5))); env.close(); auto [tx, keylet] = vault.create( @@ -2362,7 +2358,7 @@ class Vault_test : public beast::unit_test::suite env(credentials::create(depositor, credIssuer1, credType)); env(credentials::accept(depositor, credIssuer1, credType)); env(credentials::create(charlie, credIssuer1, credType)); - env(credentials::accept(charlie, credIssuer1, credType)); + // charlie's credential not accepted env.close(); auto credSle = env.le(credKeylet); BEAST_EXPECT(credSle != nullptr); @@ -2376,7 +2372,7 @@ class Vault_test : public beast::unit_test::suite tx = vault.deposit( {.depositor = charlie, .id = keylet.key, .amount = asset(50)}); - env(tx, ter{tecINSUFFICIENT_FUNDS}); + env(tx, ter{tecNO_AUTH}); env.close(); } @@ -2384,6 +2380,8 @@ class Vault_test : public beast::unit_test::suite testcase("private vault depositor lost authorization"); env(credentials::deleteCred( credIssuer1, depositor, credIssuer1, credType)); + env(credentials::deleteCred( + credIssuer1, charlie, credIssuer1, credType)); env.close(); auto credSle = env.le(credKeylet); BEAST_EXPECT(credSle == nullptr); @@ -2396,18 +2394,84 @@ class Vault_test : public beast::unit_test::suite env.close(); } - { - testcase("private vault depositor new authorization"); - env(credentials::create(depositor, credIssuer2, credType)); - env(credentials::accept(depositor, credIssuer2, credType)); - env.close(); + auto const shares = [&env, keylet = keylet, this]() -> Asset { + auto const vault = env.le(keylet); + BEAST_EXPECT(vault != nullptr); + return MPTIssue(vault->at(sfShareMPTID)); + }(); - auto tx = vault.deposit( - {.depositor = depositor, - .id = keylet.key, - .amount = asset(50)}); - env(tx); - env.close(); + { + testcase("private vault expired authorization"); + uint32_t const closeTime = env.current() + ->info() + .parentCloseTime.time_since_epoch() + .count(); + { + auto tx0 = + credentials::create(depositor, credIssuer2, credType); + tx0[sfExpiration] = closeTime + 20; + env(tx0); + tx0 = credentials::create(charlie, credIssuer2, credType); + tx0[sfExpiration] = closeTime + 20; + env(tx0); + env.close(); + + env(credentials::accept(depositor, credIssuer2, credType)); + env(credentials::accept(charlie, credIssuer2, credType)); + env.close(); + } + + { + auto tx1 = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(50)}); + env(tx1); + env.close(); + + auto const tokenKeylet = keylet::mptoken( + shares.get().getMptID(), depositor.id()); + BEAST_EXPECT(env.le(tokenKeylet) != nullptr); + } + + { + // time advance + env.close(); + env.close(); + env.close(); + + auto const credsKeylet = + credentials::keylet(depositor, credIssuer2, credType); + BEAST_EXPECT(env.le(credsKeylet) != nullptr); + + auto tx2 = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(1)}); + env(tx2, ter{tecEXPIRED}); + env.close(); + + BEAST_EXPECT(env.le(credsKeylet) == nullptr); + } + + { + auto const credsKeylet = + credentials::keylet(charlie, credIssuer2, credType); + BEAST_EXPECT(env.le(credsKeylet) != nullptr); + auto const tokenKeylet = keylet::mptoken( + shares.get().getMptID(), charlie.id()); + BEAST_EXPECT(env.le(tokenKeylet) == nullptr); + + auto tx3 = vault.deposit( + {.depositor = charlie, + .id = keylet.key, + .amount = asset(2)}); + env(tx3, ter{tecEXPIRED}); + + env.close(); + BEAST_EXPECT(env.le(credsKeylet) == nullptr); + BEAST_EXPECT(env.le(tokenKeylet) == nullptr); + } } { @@ -2455,9 +2519,11 @@ class Vault_test : public beast::unit_test::suite void testWithDomainCheckXRP() { + using namespace test::jtx; + testcase("private XRP vault"); - Env env{*this, supported_amendments() | featureSingleAssetVault}; + Env env{*this, testable_amendments() | featureSingleAssetVault}; Account owner{"owner"}; Account depositor{"depositor"}; Account alice{"charlie"}; @@ -2560,7 +2626,7 @@ class Vault_test : public beast::unit_test::suite using namespace test::jtx; testcase("fail pseudo-account allocation"); - Env env{*this, supported_amendments() | featureSingleAssetVault}; + Env env{*this, testable_amendments() | featureSingleAssetVault}; Account const owner{"owner"}; Vault vault{env}; env.fund(XRP(1000), owner); @@ -2586,8 +2652,10 @@ class Vault_test : public beast::unit_test::suite void testRPC() { + using namespace test::jtx; + testcase("RPC"); - Env env{*this, supported_amendments() | featureSingleAssetVault}; + Env env{*this, testable_amendments() | featureSingleAssetVault}; Account const owner{"owner"}; Account const issuer{"issuer"}; Vault vault{env}; diff --git a/src/test/app/XChain_test.cpp b/src/test/app/XChain_test.cpp index 85cd636b3d..311ddda59b 100644 --- a/src/test/app/XChain_test.cpp +++ b/src/test/app/XChain_test.cpp @@ -192,7 +192,7 @@ struct SEnv }; // XEnv class used for XChain tests. The only difference with SEnv is that it -// funds some default accounts, and that it enables `supported_amendments() | +// funds some default accounts, and that it enables `testable_amendments() | // FeatureBitset{featureXChainBridge}` by default. // ----------------------------------------------------------------------------- template @@ -526,7 +526,7 @@ struct XChain_test : public beast::unit_test::suite, // coverage test: BridgeCreate::preflight() - create bridge when feature // disabled. { - Env env(*this, supported_amendments() - featureXChainBridge); + Env env(*this, testable_amendments() - featureXChainBridge); env(create_bridge(Account::master, jvb), ter(temDISABLED)); } diff --git a/src/test/app/tx/apply_test.cpp b/src/test/app/tx/apply_test.cpp index 44a2c10b4e..0f5ccf5a55 100644 --- a/src/test/app/tx/apply_test.cpp +++ b/src/test/app/tx/apply_test.cpp @@ -55,7 +55,7 @@ public: { test::jtx::Env no_fully_canonical( *this, - test::jtx::supported_amendments() - + test::jtx::testable_amendments() - featureRequireFullyCanonicalSig); Validity valid = checkValidity( @@ -71,7 +71,7 @@ public: { test::jtx::Env fully_canonical( - *this, test::jtx::supported_amendments()); + *this, test::jtx::testable_amendments()); Validity valid = checkValidity( fully_canonical.app().getHashRouter(), diff --git a/src/test/basics/Buffer_test.cpp b/src/test/basics/Buffer_test.cpp index 43ca048d7f..c59805f569 100644 --- a/src/test/basics/Buffer_test.cpp +++ b/src/test/basics/Buffer_test.cpp @@ -98,8 +98,7 @@ struct Buffer_test : beast::unit_test::suite x = b0; BEAST_EXPECT(x == b0); BEAST_EXPECT(sane(x)); -#if defined(__clang__) && (!defined(__APPLE__) && (__clang_major__ >= 7)) || \ - (defined(__APPLE__) && (__apple_build_version__ >= 10010043)) +#if defined(__clang__) #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wself-assign-overloaded" #endif @@ -111,8 +110,7 @@ struct Buffer_test : beast::unit_test::suite BEAST_EXPECT(y == b3); BEAST_EXPECT(sane(y)); -#if defined(__clang__) && (!defined(__APPLE__) && (__clang_major__ >= 7)) || \ - (defined(__APPLE__) && (__apple_build_version__ >= 10010043)) +#if defined(__clang__) #pragma clang diagnostic pop #endif } diff --git a/src/test/basics/RangeSet_test.cpp b/src/test/basics/RangeSet_test.cpp deleted file mode 100644 index e0136ab890..0000000000 --- a/src/test/basics/RangeSet_test.cpp +++ /dev/null @@ -1,144 +0,0 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2012, 2013 Ripple Labs Inc. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - -#include -#include - -namespace ripple { -class RangeSet_test : public beast::unit_test::suite -{ -public: - void - testPrevMissing() - { - testcase("prevMissing"); - - // Set will include: - // [ 0, 5] - // [10,15] - // [20,25] - // etc... - - RangeSet set; - for (std::uint32_t i = 0; i < 10; ++i) - set.insert(range(10 * i, 10 * i + 5)); - - for (std::uint32_t i = 1; i < 100; ++i) - { - std::optional expected; - // no prev missing in domain for i <= 6 - if (i > 6) - { - std::uint32_t const oneBelowRange = (10 * (i / 10)) - 1; - - expected = ((i % 10) > 6) ? (i - 1) : oneBelowRange; - } - BEAST_EXPECT(prevMissing(set, i) == expected); - } - } - - void - testToString() - { - testcase("toString"); - - RangeSet set; - BEAST_EXPECT(to_string(set) == "empty"); - - set.insert(1); - BEAST_EXPECT(to_string(set) == "1"); - - set.insert(range(4u, 6u)); - BEAST_EXPECT(to_string(set) == "1,4-6"); - - set.insert(2); - BEAST_EXPECT(to_string(set) == "1-2,4-6"); - - set.erase(range(4u, 5u)); - BEAST_EXPECT(to_string(set) == "1-2,6"); - } - - void - testFromString() - { - testcase("fromString"); - - RangeSet set; - - BEAST_EXPECT(!from_string(set, "")); - BEAST_EXPECT(boost::icl::length(set) == 0); - - BEAST_EXPECT(!from_string(set, "#")); - BEAST_EXPECT(boost::icl::length(set) == 0); - - BEAST_EXPECT(!from_string(set, ",")); - BEAST_EXPECT(boost::icl::length(set) == 0); - - BEAST_EXPECT(!from_string(set, ",-")); - BEAST_EXPECT(boost::icl::length(set) == 0); - - BEAST_EXPECT(!from_string(set, "1,,2")); - BEAST_EXPECT(boost::icl::length(set) == 0); - - BEAST_EXPECT(from_string(set, "1")); - BEAST_EXPECT(boost::icl::length(set) == 1); - BEAST_EXPECT(boost::icl::first(set) == 1); - - BEAST_EXPECT(from_string(set, "1,1")); - BEAST_EXPECT(boost::icl::length(set) == 1); - BEAST_EXPECT(boost::icl::first(set) == 1); - - BEAST_EXPECT(from_string(set, "1-1")); - BEAST_EXPECT(boost::icl::length(set) == 1); - BEAST_EXPECT(boost::icl::first(set) == 1); - - BEAST_EXPECT(from_string(set, "1,4-6")); - BEAST_EXPECT(boost::icl::length(set) == 4); - BEAST_EXPECT(boost::icl::first(set) == 1); - BEAST_EXPECT(!boost::icl::contains(set, 2)); - BEAST_EXPECT(!boost::icl::contains(set, 3)); - BEAST_EXPECT(boost::icl::contains(set, 4)); - BEAST_EXPECT(boost::icl::contains(set, 5)); - BEAST_EXPECT(boost::icl::last(set) == 6); - - BEAST_EXPECT(from_string(set, "1-2,4-6")); - BEAST_EXPECT(boost::icl::length(set) == 5); - BEAST_EXPECT(boost::icl::first(set) == 1); - BEAST_EXPECT(boost::icl::contains(set, 2)); - BEAST_EXPECT(boost::icl::contains(set, 4)); - BEAST_EXPECT(boost::icl::last(set) == 6); - - BEAST_EXPECT(from_string(set, "1-2,6")); - BEAST_EXPECT(boost::icl::length(set) == 3); - BEAST_EXPECT(boost::icl::first(set) == 1); - BEAST_EXPECT(boost::icl::contains(set, 2)); - BEAST_EXPECT(boost::icl::last(set) == 6); - } - void - run() override - { - testPrevMissing(); - testToString(); - testFromString(); - } -}; - -BEAST_DEFINE_TESTSUITE(RangeSet, ripple_basics, ripple); - -} // namespace ripple diff --git a/src/test/basics/Slice_test.cpp b/src/test/basics/Slice_test.cpp deleted file mode 100644 index 3d474def79..0000000000 --- a/src/test/basics/Slice_test.cpp +++ /dev/null @@ -1,116 +0,0 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github0.com/ripple/rippled - Copyright (c) 2012-2016 Ripple Labs Inc. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - -#include -#include - -#include -#include - -namespace ripple { -namespace test { - -struct Slice_test : beast::unit_test::suite -{ - void - run() override - { - std::uint8_t const data[] = { - 0xa8, 0xa1, 0x38, 0x45, 0x23, 0xec, 0xe4, 0x23, 0x71, 0x6d, 0x2a, - 0x18, 0xb4, 0x70, 0xcb, 0xf5, 0xac, 0x2d, 0x89, 0x4d, 0x19, 0x9c, - 0xf0, 0x2c, 0x15, 0xd1, 0xf9, 0x9b, 0x66, 0xd2, 0x30, 0xd3}; - - { - testcase("Equality & Inequality"); - - Slice const s0{}; - - BEAST_EXPECT(s0.size() == 0); - BEAST_EXPECT(s0.data() == nullptr); - BEAST_EXPECT(s0 == s0); - - // Test slices of equal and unequal size pointing to same data: - for (std::size_t i = 0; i != sizeof(data); ++i) - { - Slice const s1{data, i}; - - BEAST_EXPECT(s1.size() == i); - BEAST_EXPECT(s1.data() != nullptr); - - if (i == 0) - BEAST_EXPECT(s1 == s0); - else - BEAST_EXPECT(s1 != s0); - - for (std::size_t j = 0; j != sizeof(data); ++j) - { - Slice const s2{data, j}; - - if (i == j) - BEAST_EXPECT(s1 == s2); - else - BEAST_EXPECT(s1 != s2); - } - } - - // Test slices of equal size but pointing to different data: - std::array a; - std::array b; - - for (std::size_t i = 0; i != sizeof(data); ++i) - a[i] = b[i] = data[i]; - - BEAST_EXPECT(makeSlice(a) == makeSlice(b)); - b[7]++; - BEAST_EXPECT(makeSlice(a) != makeSlice(b)); - a[7]++; - BEAST_EXPECT(makeSlice(a) == makeSlice(b)); - } - - { - testcase("Indexing"); - - Slice const s{data, sizeof(data)}; - - for (std::size_t i = 0; i != sizeof(data); ++i) - BEAST_EXPECT(s[i] == data[i]); - } - - { - testcase("Advancing"); - - for (std::size_t i = 0; i < sizeof(data); ++i) - { - for (std::size_t j = 0; i + j < sizeof(data); ++j) - { - Slice s(data + i, sizeof(data) - i); - s += j; - - BEAST_EXPECT(s.data() == data + i + j); - BEAST_EXPECT(s.size() == sizeof(data) - i - j); - } - } - } - } -}; - -BEAST_DEFINE_TESTSUITE(Slice, ripple_basics, ripple); - -} // namespace test -} // namespace ripple diff --git a/src/test/basics/base64_test.cpp b/src/test/basics/base64_test.cpp deleted file mode 100644 index b6d67c7c06..0000000000 --- a/src/test/basics/base64_test.cpp +++ /dev/null @@ -1,82 +0,0 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2012-2018 Ripple Labs Inc. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - -// -// Copyright (c) 2016-2017 Vinnie Falco (vinnie dot falco at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/boostorg/beast -// - -#include -#include - -namespace ripple { - -class base64_test : public beast::unit_test::suite -{ -public: - void - check(std::string const& in, std::string const& out) - { - auto const encoded = base64_encode(in); - BEAST_EXPECT(encoded == out); - BEAST_EXPECT(base64_decode(encoded) == in); - } - - void - run() override - { - check("", ""); - check("f", "Zg=="); - check("fo", "Zm8="); - check("foo", "Zm9v"); - check("foob", "Zm9vYg=="); - check("fooba", "Zm9vYmE="); - check("foobar", "Zm9vYmFy"); - - check( - "Man is distinguished, not only by his reason, but by this " - "singular passion from " - "other animals, which is a lust of the mind, that by a " - "perseverance of delight " - "in the continued and indefatigable generation of knowledge, " - "exceeds the short " - "vehemence of any carnal pleasure.", - "TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dC" - "BieSB0aGlz" - "IHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYWxzLCB3aGljaCBpcyBhIG" - "x1c3Qgb2Yg" - "dGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aG" - "UgY29udGlu" - "dWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleG" - "NlZWRzIHRo" - "ZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4="); - - std::string const notBase64 = "not_base64!!"; - std::string const truncated = "not"; - BEAST_EXPECT(base64_decode(notBase64) == base64_decode(truncated)); - } -}; - -BEAST_DEFINE_TESTSUITE(base64, ripple_basics, ripple); - -} // namespace ripple diff --git a/src/test/basics/mulDiv_test.cpp b/src/test/basics/mulDiv_test.cpp deleted file mode 100644 index 61521577d9..0000000000 --- a/src/test/basics/mulDiv_test.cpp +++ /dev/null @@ -1,62 +0,0 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2012-2016 Ripple Labs Inc. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - -#include -#include - -namespace ripple { -namespace test { - -struct mulDiv_test : beast::unit_test::suite -{ - void - run() override - { - auto const max = std::numeric_limits::max(); - std::uint64_t const max32 = std::numeric_limits::max(); - - auto result = mulDiv(85, 20, 5); - BEAST_EXPECT(result && *result == 340); - result = mulDiv(20, 85, 5); - BEAST_EXPECT(result && *result == 340); - - result = mulDiv(0, max - 1, max - 3); - BEAST_EXPECT(result && *result == 0); - result = mulDiv(max - 1, 0, max - 3); - BEAST_EXPECT(result && *result == 0); - - result = mulDiv(max, 2, max / 2); - BEAST_EXPECT(result && *result == 4); - result = mulDiv(max, 1000, max / 1000); - BEAST_EXPECT(result && *result == 1000000); - result = mulDiv(max, 1000, max / 1001); - BEAST_EXPECT(result && *result == 1001000); - result = mulDiv(max32 + 1, max32 + 1, 5); - BEAST_EXPECT(result && *result == 3689348814741910323); - - // Overflow - result = mulDiv(max - 1, max - 2, 5); - BEAST_EXPECT(!result); - } -}; - -BEAST_DEFINE_TESTSUITE(mulDiv, ripple_basics, ripple); - -} // namespace test -} // namespace ripple diff --git a/src/test/basics/scope_test.cpp b/src/test/basics/scope_test.cpp deleted file mode 100644 index 654f7e0a11..0000000000 --- a/src/test/basics/scope_test.cpp +++ /dev/null @@ -1,193 +0,0 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github0.com/ripple/rippled - Copyright (c) 2021 Ripple Inc. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - -#include -#include - -namespace ripple { -namespace test { - -struct scope_test : beast::unit_test::suite -{ - void - test_scope_exit() - { - // scope_exit always executes the functor on destruction, - // unless release() is called - int i = 0; - { - scope_exit x{[&i]() { i = 1; }}; - } - BEAST_EXPECT(i == 1); - { - scope_exit x{[&i]() { i = 2; }}; - x.release(); - } - BEAST_EXPECT(i == 1); - { - scope_exit x{[&i]() { i += 2; }}; - auto x2 = std::move(x); - } - BEAST_EXPECT(i == 3); - { - scope_exit x{[&i]() { i = 4; }}; - x.release(); - auto x2 = std::move(x); - } - BEAST_EXPECT(i == 3); - { - try - { - scope_exit x{[&i]() { i = 5; }}; - throw 1; - } - catch (...) - { - } - } - BEAST_EXPECT(i == 5); - { - try - { - scope_exit x{[&i]() { i = 6; }}; - x.release(); - throw 1; - } - catch (...) - { - } - } - BEAST_EXPECT(i == 5); - } - - void - test_scope_fail() - { - // scope_fail executes the functor on destruction only - // if an exception is unwinding, unless release() is called - int i = 0; - { - scope_fail x{[&i]() { i = 1; }}; - } - BEAST_EXPECT(i == 0); - { - scope_fail x{[&i]() { i = 2; }}; - x.release(); - } - BEAST_EXPECT(i == 0); - { - scope_fail x{[&i]() { i = 3; }}; - auto x2 = std::move(x); - } - BEAST_EXPECT(i == 0); - { - scope_fail x{[&i]() { i = 4; }}; - x.release(); - auto x2 = std::move(x); - } - BEAST_EXPECT(i == 0); - { - try - { - scope_fail x{[&i]() { i = 5; }}; - throw 1; - } - catch (...) - { - } - } - BEAST_EXPECT(i == 5); - { - try - { - scope_fail x{[&i]() { i = 6; }}; - x.release(); - throw 1; - } - catch (...) - { - } - } - BEAST_EXPECT(i == 5); - } - - void - test_scope_success() - { - // scope_success executes the functor on destruction only - // if an exception is not unwinding, unless release() is called - int i = 0; - { - scope_success x{[&i]() { i = 1; }}; - } - BEAST_EXPECT(i == 1); - { - scope_success x{[&i]() { i = 2; }}; - x.release(); - } - BEAST_EXPECT(i == 1); - { - scope_success x{[&i]() { i += 2; }}; - auto x2 = std::move(x); - } - BEAST_EXPECT(i == 3); - { - scope_success x{[&i]() { i = 4; }}; - x.release(); - auto x2 = std::move(x); - } - BEAST_EXPECT(i == 3); - { - try - { - scope_success x{[&i]() { i = 5; }}; - throw 1; - } - catch (...) - { - } - } - BEAST_EXPECT(i == 3); - { - try - { - scope_success x{[&i]() { i = 6; }}; - x.release(); - throw 1; - } - catch (...) - { - } - } - BEAST_EXPECT(i == 3); - } - - void - run() override - { - test_scope_exit(); - test_scope_fail(); - test_scope_success(); - } -}; - -BEAST_DEFINE_TESTSUITE(scope, ripple_basics, ripple); - -} // namespace test -} // namespace ripple diff --git a/src/test/basics/tagged_integer_test.cpp b/src/test/basics/tagged_integer_test.cpp deleted file mode 100644 index cb15d246a6..0000000000 --- a/src/test/basics/tagged_integer_test.cpp +++ /dev/null @@ -1,258 +0,0 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright 2014, Nikolaos D. Bougalis - - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - -#include -#include - -#include - -namespace ripple { -namespace test { - -class tagged_integer_test : public beast::unit_test::suite -{ -private: - struct Tag1 - { - }; - struct Tag2 - { - }; - - // Static checks that types are not interoperable - - using TagUInt1 = tagged_integer; - using TagUInt2 = tagged_integer; - using TagUInt3 = tagged_integer; - - // Check construction of tagged_integers - static_assert( - std::is_constructible::value, - "TagUInt1 should be constructible using a std::uint32_t"); - - static_assert( - !std::is_constructible::value, - "TagUInt1 should not be constructible using a std::uint64_t"); - - static_assert( - std::is_constructible::value, - "TagUInt3 should be constructible using a std::uint32_t"); - - static_assert( - std::is_constructible::value, - "TagUInt3 should be constructible using a std::uint64_t"); - - // Check assignment of tagged_integers - static_assert( - !std::is_assignable::value, - "TagUInt1 should not be assignable with a std::uint32_t"); - - static_assert( - !std::is_assignable::value, - "TagUInt1 should not be assignable with a std::uint64_t"); - - static_assert( - !std::is_assignable::value, - "TagUInt3 should not be assignable with a std::uint32_t"); - - static_assert( - !std::is_assignable::value, - "TagUInt3 should not be assignable with a std::uint64_t"); - - static_assert( - std::is_assignable::value, - "TagUInt1 should be assignable with a TagUInt1"); - - static_assert( - !std::is_assignable::value, - "TagUInt1 should not be assignable with a TagUInt2"); - - static_assert( - std::is_assignable::value, - "TagUInt3 should be assignable with a TagUInt1"); - - static_assert( - !std::is_assignable::value, - "TagUInt1 should not be assignable with a TagUInt3"); - - static_assert( - !std::is_assignable::value, - "TagUInt3 should not be assignable with a TagUInt1"); - - // Check convertibility of tagged_integers - static_assert( - !std::is_convertible::value, - "std::uint32_t should not be convertible to a TagUInt1"); - - static_assert( - !std::is_convertible::value, - "std::uint32_t should not be convertible to a TagUInt3"); - - static_assert( - !std::is_convertible::value, - "std::uint64_t should not be convertible to a TagUInt3"); - - static_assert( - !std::is_convertible::value, - "std::uint64_t should not be convertible to a TagUInt2"); - - static_assert( - !std::is_convertible::value, - "TagUInt1 should not be convertible to TagUInt2"); - - static_assert( - !std::is_convertible::value, - "TagUInt1 should not be convertible to TagUInt3"); - - static_assert( - !std::is_convertible::value, - "TagUInt2 should not be convertible to a TagUInt3"); - -public: - void - run() override - { - using TagInt = tagged_integer; - - { - testcase("Comparison Operators"); - - TagInt const zero(0); - TagInt const one(1); - - BEAST_EXPECT(one == one); - BEAST_EXPECT(!(one == zero)); - - BEAST_EXPECT(one != zero); - BEAST_EXPECT(!(one != one)); - - BEAST_EXPECT(zero < one); - BEAST_EXPECT(!(one < zero)); - - BEAST_EXPECT(one > zero); - BEAST_EXPECT(!(zero > one)); - - BEAST_EXPECT(one >= one); - BEAST_EXPECT(one >= zero); - BEAST_EXPECT(!(zero >= one)); - - BEAST_EXPECT(zero <= one); - BEAST_EXPECT(zero <= zero); - BEAST_EXPECT(!(one <= zero)); - } - - { - testcase("Increment/Decrement Operators"); - TagInt const zero(0); - TagInt const one(1); - TagInt a{0}; - ++a; - BEAST_EXPECT(a == one); - --a; - BEAST_EXPECT(a == zero); - a++; - BEAST_EXPECT(a == one); - a--; - BEAST_EXPECT(a == zero); - } - - { - testcase("Arithmetic Operators"); - TagInt a{-2}; - BEAST_EXPECT(+a == TagInt{-2}); - BEAST_EXPECT(-a == TagInt{2}); - BEAST_EXPECT(TagInt{-3} + TagInt{4} == TagInt{1}); - BEAST_EXPECT(TagInt{-3} - TagInt{4} == TagInt{-7}); - BEAST_EXPECT(TagInt{-3} * TagInt{4} == TagInt{-12}); - BEAST_EXPECT(TagInt{8} / TagInt{4} == TagInt{2}); - BEAST_EXPECT(TagInt{7} % TagInt{4} == TagInt{3}); - - BEAST_EXPECT(~TagInt{8} == TagInt{~TagInt::value_type{8}}); - BEAST_EXPECT((TagInt{6} & TagInt{3}) == TagInt{2}); - BEAST_EXPECT((TagInt{6} | TagInt{3}) == TagInt{7}); - BEAST_EXPECT((TagInt{6} ^ TagInt{3}) == TagInt{5}); - - BEAST_EXPECT((TagInt{4} << TagInt{2}) == TagInt{16}); - BEAST_EXPECT((TagInt{16} >> TagInt{2}) == TagInt{4}); - } - { - testcase("Assignment Operators"); - TagInt a{-2}; - TagInt b{0}; - b = a; - BEAST_EXPECT(b == TagInt{-2}); - - // -3 + 4 == 1 - a = TagInt{-3}; - a += TagInt{4}; - BEAST_EXPECT(a == TagInt{1}); - - // -3 - 4 == -7 - a = TagInt{-3}; - a -= TagInt{4}; - BEAST_EXPECT(a == TagInt{-7}); - - // -3 * 4 == -12 - a = TagInt{-3}; - a *= TagInt{4}; - BEAST_EXPECT(a == TagInt{-12}); - - // 8/4 == 2 - a = TagInt{8}; - a /= TagInt{4}; - BEAST_EXPECT(a == TagInt{2}); - - // 7 % 4 == 3 - a = TagInt{7}; - a %= TagInt{4}; - BEAST_EXPECT(a == TagInt{3}); - - // 6 & 3 == 2 - a = TagInt{6}; - a /= TagInt{3}; - BEAST_EXPECT(a == TagInt{2}); - - // 6 | 3 == 7 - a = TagInt{6}; - a |= TagInt{3}; - BEAST_EXPECT(a == TagInt{7}); - - // 6 ^ 3 == 5 - a = TagInt{6}; - a ^= TagInt{3}; - BEAST_EXPECT(a == TagInt{5}); - - // 4 << 2 == 16 - a = TagInt{4}; - a <<= TagInt{2}; - BEAST_EXPECT(a == TagInt{16}); - - // 16 >> 2 == 4 - a = TagInt{16}; - a >>= TagInt{2}; - BEAST_EXPECT(a == TagInt{4}); - } - } -}; - -BEAST_DEFINE_TESTSUITE(tagged_integer, ripple_basics, ripple); - -} // namespace test -} // namespace ripple diff --git a/src/test/consensus/NegativeUNL_test.cpp b/src/test/consensus/NegativeUNL_test.cpp index 7eb05e68bb..56558f525f 100644 --- a/src/test/consensus/NegativeUNL_test.cpp +++ b/src/test/consensus/NegativeUNL_test.cpp @@ -227,7 +227,7 @@ class NegativeUNL_test : public beast::unit_test::suite testcase("Create UNLModify Tx and apply to ledgers"); - jtx::Env env(*this, jtx::supported_amendments() | featureNegativeUNL); + jtx::Env env(*this, jtx::testable_amendments() | featureNegativeUNL); std::vector publicKeys = createPublicKeys(3); // genesis ledger auto l = std::make_shared( @@ -526,7 +526,7 @@ class NegativeUNLNoAmendment_test : public beast::unit_test::suite { testcase("No negative UNL amendment"); - jtx::Env env(*this, jtx::supported_amendments() - featureNegativeUNL); + jtx::Env env(*this, jtx::testable_amendments() - featureNegativeUNL); std::vector publicKeys = createPublicKeys(1); // genesis ledger auto l = std::make_shared( @@ -582,7 +582,7 @@ struct NetworkHistory }; NetworkHistory(beast::unit_test::suite& suite, Parameter const& p) - : env(suite, jtx::supported_amendments() | featureNegativeUNL) + : env(suite, jtx::testable_amendments() | featureNegativeUNL) , param(p) , validations(env.app().getValidations()) { diff --git a/src/test/jtx.h b/src/test/jtx.h index 6b73ca63ec..6347b9dcf9 100644 --- a/src/test/jtx.h +++ b/src/test/jtx.h @@ -22,6 +22,7 @@ // Convenience header that includes everything +#include #include #include #include @@ -31,11 +32,15 @@ #include #include #include +#include #include #include +#include #include #include #include +#include +#include #include #include #include @@ -50,6 +55,7 @@ #include #include #include +#include #include #include #include diff --git a/src/test/jtx/AMM.h b/src/test/jtx/AMM.h index 71e2e5f34c..07e60369fe 100644 --- a/src/test/jtx/AMM.h +++ b/src/test/jtx/AMM.h @@ -127,7 +127,6 @@ class AMM STAmount const asset1_; STAmount const asset2_; uint256 const ammID_; - IOUAmount const initialLPTokens_; bool log_; bool doClose_; // Predict next purchase price @@ -140,6 +139,7 @@ class AMM std::uint32_t const fee_; AccountID const ammAccount_; Issue const lptIssue_; + IOUAmount const initialLPTokens_; public: AMM(Env& env, @@ -196,6 +196,12 @@ public: Issue const& issue2, std::optional const& account = std::nullopt) const; + std::tuple + balances(std::optional const& account = std::nullopt) const + { + return balances(asset1_.get(), asset2_.get(), account); + } + [[nodiscard]] bool expectLPTokens(AccountID const& account, IOUAmount const& tokens) const; @@ -430,6 +436,9 @@ private: [[nodiscard]] bool expectAuctionSlot(auto&& cb) const; + + IOUAmount + initialTokens(); }; namespace amm { diff --git a/src/test/jtx/AMMTest.h b/src/test/jtx/AMMTest.h index 5ff2d21a19..17011d7633 100644 --- a/src/test/jtx/AMMTest.h +++ b/src/test/jtx/AMMTest.h @@ -35,6 +35,15 @@ class AMM; enum class Fund { All, Acct, Gw, IOUOnly }; +struct TestAMMArg +{ + std::optional> pool = std::nullopt; + std::uint16_t tfee = 0; + std::optional ter = std::nullopt; + std::vector features = {testable_amendments()}; + bool noLog = false; +}; + void fund( jtx::Env& env, @@ -86,7 +95,12 @@ protected: std::optional> const& pool = std::nullopt, std::uint16_t tfee = 0, std::optional const& ter = std::nullopt, - std::vector const& features = {supported_amendments()}); + std::vector const& features = {testable_amendments()}); + + void + testAMM( + std::function&& cb, + TestAMMArg const& arg); }; class AMMTest : public jtx::AMMTestBase diff --git a/src/test/jtx/Env.h b/src/test/jtx/Env.h index 3a2171a420..68d8d3e53f 100644 --- a/src/test/jtx/Env.h +++ b/src/test/jtx/Env.h @@ -71,10 +71,10 @@ noripple(Account const& account, Args const&... args) } inline FeatureBitset -supported_amendments() +testable_amendments() { static FeatureBitset const ids = [] { - auto const& sa = ripple::detail::supportedAmendments(); + auto const& sa = allAmendments(); std::vector feats; feats.reserve(sa.size()); for (auto const& [s, vote] : sa) @@ -84,7 +84,7 @@ supported_amendments() feats.push_back(*f); else Throw( - "Unknown feature: " + s + " in supportedAmendments."); + "Unknown feature: " + s + " in allAmendments."); } return FeatureBitset(feats); }(); @@ -236,7 +236,7 @@ public: beast::severities::Severity thresh = beast::severities::kError) : Env(suite_, std::move(config), - supported_amendments(), + testable_amendments(), std::move(logs), thresh) { @@ -478,6 +478,12 @@ public: PrettyAmount balance(Account const& account, MPTIssue const& mptIssue) const; + /** Returns the IOU limit on an account. + Returns 0 if the trust line does not exist. + */ + PrettyAmount + limit(Account const& account, Issue const& issue) const; + /** Return the number of objects owned by an account. * Returns 0 if the account does not exist. */ @@ -595,13 +601,16 @@ public: } /** Return metadata for the last JTx. - - Effects: - - The open ledger is closed as if by a call - to close(). The metadata for the last - transaction ID, if any, is returned. - */ + * + * NOTE: this has a side effect of closing the open ledger. + * The ledger will only be closed if it includes transactions. + * + * Effects: + * + * The open ledger is closed as if by a call + * to close(). The metadata for the last + * transaction ID, if any, is returned. + */ std::shared_ptr meta(); @@ -625,6 +634,12 @@ public: void disableFeature(uint256 const feature); + bool + enabled(uint256 feature) const + { + return current()->rules().enabled(feature); + } + private: void fund(bool setDefaultRipple, STAmount const& amount, Account const& account); diff --git a/src/test/jtx/Env_test.cpp b/src/test/jtx/Env_test.cpp index f32343d6dd..2be20d6e33 100644 --- a/src/test/jtx/Env_test.cpp +++ b/src/test/jtx/Env_test.cpp @@ -265,7 +265,7 @@ public: { using namespace jtx; - Env env{*this, supported_amendments() | fixMasterKeyAsRegularKey}; + Env env{*this, testable_amendments() | fixMasterKeyAsRegularKey}; Account const alice("alice", KeyType::ed25519); Account const bob("bob", KeyType::secp256k1); Account const carol("carol"); @@ -776,7 +776,7 @@ public: { testcase("Env features"); using namespace jtx; - auto const supported = supported_amendments(); + auto const supported = testable_amendments(); // this finds a feature that is not in // the supported amendments list and tests that it can be @@ -827,7 +827,7 @@ public: } auto const missingSomeFeatures = - supported_amendments() - featureMultiSignReserve - featureFlow; + testable_amendments() - featureMultiSignReserve - featureFlow; BEAST_EXPECT(missingSomeFeatures.count() == (supported.count() - 2)); { // a Env supported_features_except is missing *only* those features @@ -887,7 +887,7 @@ public: // add a feature that is NOT in the supported amendments list // along with all supported amendments // the unsupported features should be enabled - Env env{*this, supported_amendments().set(*neverSupportedFeat)}; + Env env{*this, testable_amendments().set(*neverSupportedFeat)}; // this app will have all supported amendments and then the // one additional never supported feature flag diff --git a/src/test/jtx/SignerUtils.h b/src/test/jtx/SignerUtils.h new file mode 100644 index 0000000000..7b1ae5007c --- /dev/null +++ b/src/test/jtx/SignerUtils.h @@ -0,0 +1,56 @@ +#ifndef RIPPLE_TEST_JTX_SIGNERUTILS_H_INCLUDED +#define RIPPLE_TEST_JTX_SIGNERUTILS_H_INCLUDED + +#include + +#include + +namespace ripple { +namespace test { +namespace jtx { + +struct Reg +{ + Account acct; + Account sig; + + Reg(Account const& masterSig) : acct(masterSig), sig(masterSig) + { + } + + Reg(Account const& acct_, Account const& regularSig) + : acct(acct_), sig(regularSig) + { + } + + Reg(char const* masterSig) : acct(masterSig), sig(masterSig) + { + } + + Reg(char const* acct_, char const* regularSig) + : acct(acct_), sig(regularSig) + { + } + + bool + operator<(Reg const& rhs) const + { + return acct < rhs.acct; + } +}; + +// Utility function to sort signers +inline void +sortSigners(std::vector& signers) +{ + std::sort( + signers.begin(), signers.end(), [](Reg const& lhs, Reg const& rhs) { + return lhs.acct < rhs.acct; + }); +} + +} // namespace jtx +} // namespace test +} // namespace ripple + +#endif diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index 11c13543e4..aa93f3236f 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -23,6 +23,7 @@ #include #include +#include #include #include #include @@ -33,6 +34,14 @@ #include +#if (defined(__clang_major__) && __clang_major__ < 15) +#include +using source_location = std::experimental::source_location; +#else +#include +using std::source_location; +#endif + namespace ripple { namespace test { namespace jtx { @@ -491,55 +500,6 @@ expectLedgerEntryRoot( Account const& acct, STAmount const& expectedValue); -/* Escrow */ -/******************************************************************************/ - -Json::Value -escrow(AccountID const& account, AccountID const& to, STAmount const& amount); - -inline Json::Value -escrow(Account const& account, Account const& to, STAmount const& amount) -{ - return escrow(account.id(), to.id(), amount); -} - -Json::Value -finish(AccountID const& account, AccountID const& from, std::uint32_t seq); - -inline Json::Value -finish(Account const& account, Account const& from, std::uint32_t seq) -{ - return finish(account.id(), from.id(), seq); -} - -Json::Value -cancel(AccountID const& account, Account const& from, std::uint32_t seq); - -inline Json::Value -cancel(Account const& account, Account const& from, std::uint32_t seq) -{ - return cancel(account.id(), from, seq); -} - -std::array constexpr cb1 = { - {0xA0, 0x25, 0x80, 0x20, 0xE3, 0xB0, 0xC4, 0x42, 0x98, 0xFC, - 0x1C, 0x14, 0x9A, 0xFB, 0xF4, 0xC8, 0x99, 0x6F, 0xB9, 0x24, - 0x27, 0xAE, 0x41, 0xE4, 0x64, 0x9B, 0x93, 0x4C, 0xA4, 0x95, - 0x99, 0x1B, 0x78, 0x52, 0xB8, 0x55, 0x81, 0x01, 0x00}}; - -// A PreimageSha256 fulfillments and its associated condition. -std::array const fb1 = {{0xA0, 0x02, 0x80, 0x00}}; - -/** Set the "FinishAfter" time tag on a JTx */ -auto const finish_time = JTxFieldWrapper(sfFinishAfter); - -/** Set the "CancelAfter" time tag on a JTx */ -auto const cancel_time = JTxFieldWrapper(sfCancelAfter); - -auto const condition = JTxFieldWrapper(sfCondition); - -auto const fulfillment = JTxFieldWrapper(sfFulfillment); - /* Payment Channel */ /******************************************************************************/ @@ -640,7 +600,6 @@ create(A const& account, A const& dest, STAmount const& sendMax) jv[sfSendMax.jsonName] = sendMax.getJson(JsonOptions::none); jv[sfDestination.jsonName] = to_string(dest); jv[sfTransactionType.jsonName] = jss::CheckCreate; - jv[sfFlags.jsonName] = tfUniversal; return jv; } // clang-format on @@ -656,6 +615,102 @@ create( } // namespace check +static constexpr FeeLevel64 baseFeeLevel{256}; +static constexpr FeeLevel64 minEscalationFeeLevel = baseFeeLevel * 500; + +template +void +checkMetrics( + Suite& test, + jtx::Env& env, + std::size_t expectedCount, + std::optional expectedMaxCount, + std::size_t expectedInLedger, + std::size_t expectedPerLedger, + std::uint64_t expectedMinFeeLevel = baseFeeLevel.fee(), + std::uint64_t expectedMedFeeLevel = minEscalationFeeLevel.fee(), + source_location const location = source_location::current()) +{ + int line = location.line(); + char const* file = location.file_name(); + FeeLevel64 const expectedMin{expectedMinFeeLevel}; + FeeLevel64 const expectedMed{expectedMedFeeLevel}; + auto const metrics = env.app().getTxQ().getMetrics(*env.current()); + using namespace std::string_literals; + + metrics.referenceFeeLevel == baseFeeLevel + ? test.pass() + : test.fail( + "reference: "s + + std::to_string(metrics.referenceFeeLevel.value()) + "/" + + std::to_string(baseFeeLevel.value()), + file, + line); + + metrics.txCount == expectedCount + ? test.pass() + : test.fail( + "txCount: "s + std::to_string(metrics.txCount) + "/" + + std::to_string(expectedCount), + file, + line); + + metrics.txQMaxSize == expectedMaxCount + ? test.pass() + : test.fail( + "txQMaxSize: "s + std::to_string(metrics.txQMaxSize.value_or(0)) + + "/" + std::to_string(expectedMaxCount.value_or(0)), + file, + line); + + metrics.txInLedger == expectedInLedger + ? test.pass() + : test.fail( + "txInLedger: "s + std::to_string(metrics.txInLedger) + "/" + + std::to_string(expectedInLedger), + file, + line); + + metrics.txPerLedger == expectedPerLedger + ? test.pass() + : test.fail( + "txPerLedger: "s + std::to_string(metrics.txPerLedger) + "/" + + std::to_string(expectedPerLedger), + file, + line); + + metrics.minProcessingFeeLevel == expectedMin + ? test.pass() + : test.fail( + "minProcessingFeeLevel: "s + + std::to_string(metrics.minProcessingFeeLevel.value()) + "/" + + std::to_string(expectedMin.value()), + file, + line); + + metrics.medFeeLevel == expectedMed + ? test.pass() + : test.fail( + "medFeeLevel: "s + std::to_string(metrics.medFeeLevel.value()) + + "/" + std::to_string(expectedMed.value()), + file, + line); + + auto const expectedCurFeeLevel = expectedInLedger > expectedPerLedger + ? expectedMed * expectedInLedger * expectedInLedger / + (expectedPerLedger * expectedPerLedger) + : metrics.referenceFeeLevel; + + metrics.openLedgerFeeLevel == expectedCurFeeLevel + ? test.pass() + : test.fail( + "openLedgerFeeLevel: "s + + std::to_string(metrics.openLedgerFeeLevel.value()) + "/" + + std::to_string(expectedCurFeeLevel.value()), + file, + line); +} + } // namespace jtx } // namespace test } // namespace ripple diff --git a/src/test/jtx/acctdelete.h b/src/test/jtx/acctdelete.h index 98a23c6de2..21d00cb727 100644 --- a/src/test/jtx/acctdelete.h +++ b/src/test/jtx/acctdelete.h @@ -23,6 +23,8 @@ #include #include +#include + namespace ripple { namespace test { namespace jtx { @@ -31,6 +33,15 @@ namespace jtx { Json::Value acctdelete(Account const& account, Account const& dest); +// Close the ledger until the ledger sequence is large enough to close +// the account. If margin is specified, close the ledger so `margin` +// more closes are needed +void +incLgrSeqForAccDel( + jtx::Env& env, + jtx::Account const& acc, + std::uint32_t margin = 0); + } // namespace jtx } // namespace test diff --git a/src/test/jtx/batch.h b/src/test/jtx/batch.h new file mode 100644 index 0000000000..ab235c293f --- /dev/null +++ b/src/test/jtx/batch.h @@ -0,0 +1,169 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TEST_JTX_BATCH_H_INCLUDED +#define RIPPLE_TEST_JTX_BATCH_H_INCLUDED + +#include +#include +#include +#include +#include + +#include + +#include "test/jtx/SignerUtils.h" + +#include +#include +#include + +namespace ripple { +namespace test { +namespace jtx { + +/** Batch operations */ +namespace batch { + +/** Calculate Batch Fee. */ +XRPAmount +calcBatchFee( + jtx::Env const& env, + uint32_t const& numSigners, + uint32_t const& txns = 0); + +/** Batch. */ +Json::Value +outer( + jtx::Account const& account, + uint32_t seq, + STAmount const& fee, + std::uint32_t flags); + +/** Adds a new Batch Txn on a JTx and autofills. */ +class inner +{ +private: + Json::Value txn_; + std::uint32_t seq_; + std::optional ticket_; + +public: + inner( + Json::Value const& txn, + std::uint32_t const& sequence, + std::optional const& ticket = std::nullopt, + std::optional const& fee = std::nullopt) + : txn_(txn), seq_(sequence), ticket_(ticket) + { + txn_[jss::SigningPubKey] = ""; + txn_[jss::Sequence] = seq_; + txn_[jss::Fee] = "0"; + txn_[jss::Flags] = txn_[jss::Flags].asUInt() | tfInnerBatchTxn; + + // Optionally set ticket sequence + if (ticket_.has_value()) + { + txn_[jss::Sequence] = 0; + txn_[sfTicketSequence.jsonName] = *ticket_; + } + } + + void + operator()(Env&, JTx& jtx) const; + + Json::Value& + operator[](Json::StaticString const& key) + { + return txn_[key]; + } + + void + removeMember(Json::StaticString const& key) + { + txn_.removeMember(key); + } + + Json::Value const& + getTxn() const + { + return txn_; + } +}; + +/** Set a batch signature on a JTx. */ +class sig +{ +public: + std::vector signers; + + sig(std::vector signers_) : signers(std::move(signers_)) + { + sortSigners(signers); + } + + template + requires std::convertible_to + explicit sig(AccountType&& a0, Accounts&&... aN) + : signers{std::forward(a0), std::forward(aN)...} + { + sortSigners(signers); + } + + void + operator()(Env&, JTx& jt) const; +}; + +/** Set a batch nested multi-signature on a JTx. */ +class msig +{ +public: + Account master; + std::vector signers; + + msig(Account const& masterAccount, std::vector signers_) + : master(masterAccount), signers(std::move(signers_)) + { + sortSigners(signers); + } + + template + requires std::convertible_to + explicit msig( + Account const& masterAccount, + AccountType&& a0, + Accounts&&... aN) + : master(masterAccount) + , signers{std::forward(a0), std::forward(aN)...} + { + sortSigners(signers); + } + + void + operator()(Env&, JTx& jt) const; +}; + +} // namespace batch + +} // namespace jtx + +} // namespace test +} // namespace ripple + +#endif diff --git a/src/test/core/CryptoPRNG_test.cpp b/src/test/jtx/domain.h similarity index 60% rename from src/test/core/CryptoPRNG_test.cpp rename to src/test/jtx/domain.h index 21924e582c..4af270c1d0 100644 --- a/src/test/core/CryptoPRNG_test.cpp +++ b/src/test/jtx/domain.h @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ /* This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2012-2017 Ripple Labs Inc. + Copyright (c) 2025 Ripple Labs Inc. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above @@ -17,44 +17,29 @@ */ //============================================================================== +#pragma once + #include -#include -#include - namespace ripple { +namespace test { +namespace jtx { -class CryptoPRNG_test : public beast::unit_test::suite +/** Set the domain on a JTx. */ +class domain { - void - testGetValues() - { - testcase("Get Values"); - try - { - auto& engine = crypto_prng(); - auto rand_val = engine(); - BEAST_EXPECT(rand_val >= engine.min()); - BEAST_EXPECT(rand_val <= engine.max()); - - uint16_t twoByte{0}; - engine(&twoByte, sizeof(uint16_t)); - pass(); - } - catch (std::exception&) - { - fail(); - } - } +private: + uint256 v_; public: - void - run() override + explicit domain(uint256 const& v) : v_(v) { - testGetValues(); } + + void + operator()(Env&, JTx& jt) const; }; -BEAST_DEFINE_TESTSUITE(CryptoPRNG, core, ripple); - +} // namespace jtx +} // namespace test } // namespace ripple diff --git a/src/test/jtx/envconfig.h b/src/test/jtx/envconfig.h index f22c5743e7..432ef28ff6 100644 --- a/src/test/jtx/envconfig.h +++ b/src/test/jtx/envconfig.h @@ -127,6 +127,11 @@ addGrpcConfigWithSecureGateway( std::unique_ptr, std::string const& secureGateway); +std::unique_ptr +makeConfig( + std::map extraTxQ = {}, + std::map extraVoting = {}); + } // namespace jtx } // namespace test } // namespace ripple diff --git a/src/test/jtx/escrow.h b/src/test/jtx/escrow.h new file mode 100644 index 0000000000..483db578b0 --- /dev/null +++ b/src/test/jtx/escrow.h @@ -0,0 +1,114 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2019 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TEST_JTX_ESCROW_H_INCLUDED +#define RIPPLE_TEST_JTX_ESCROW_H_INCLUDED + +#include +#include +#include +#include +#include + +#include + +namespace ripple { +namespace test { +namespace jtx { + +/** Escrow operations. */ +namespace escrow { + +Json::Value +create(AccountID const& account, AccountID const& to, STAmount const& amount); + +inline Json::Value +create(Account const& account, Account const& to, STAmount const& amount) +{ + return create(account.id(), to.id(), amount); +} + +Json::Value +finish(AccountID const& account, AccountID const& from, std::uint32_t seq); + +inline Json::Value +finish(Account const& account, Account const& from, std::uint32_t seq) +{ + return finish(account.id(), from.id(), seq); +} + +Json::Value +cancel(AccountID const& account, Account const& from, std::uint32_t seq); + +inline Json::Value +cancel(Account const& account, Account const& from, std::uint32_t seq) +{ + return cancel(account.id(), from, seq); +} + +Rate +rate(Env& env, Account const& account, std::uint32_t const& seq); + +// A PreimageSha256 fulfillments and its associated condition. +std::array const fb1 = {{0xA0, 0x02, 0x80, 0x00}}; + +std::array const cb1 = { + {0xA0, 0x25, 0x80, 0x20, 0xE3, 0xB0, 0xC4, 0x42, 0x98, 0xFC, + 0x1C, 0x14, 0x9A, 0xFB, 0xF4, 0xC8, 0x99, 0x6F, 0xB9, 0x24, + 0x27, 0xAE, 0x41, 0xE4, 0x64, 0x9B, 0x93, 0x4C, 0xA4, 0x95, + 0x99, 0x1B, 0x78, 0x52, 0xB8, 0x55, 0x81, 0x01, 0x00}}; + +// Another PreimageSha256 fulfillments and its associated condition. +std::array const fb2 = { + {0xA0, 0x05, 0x80, 0x03, 0x61, 0x61, 0x61}}; + +std::array const cb2 = { + {0xA0, 0x25, 0x80, 0x20, 0x98, 0x34, 0x87, 0x6D, 0xCF, 0xB0, + 0x5C, 0xB1, 0x67, 0xA5, 0xC2, 0x49, 0x53, 0xEB, 0xA5, 0x8C, + 0x4A, 0xC8, 0x9B, 0x1A, 0xDF, 0x57, 0xF2, 0x8F, 0x2F, 0x9D, + 0x09, 0xAF, 0x10, 0x7E, 0xE8, 0xF0, 0x81, 0x01, 0x03}}; + +// Another PreimageSha256 fulfillment and its associated condition. +std::array const fb3 = { + {0xA0, 0x06, 0x80, 0x04, 0x6E, 0x69, 0x6B, 0x62}}; + +std::array const cb3 = { + {0xA0, 0x25, 0x80, 0x20, 0x6E, 0x4C, 0x71, 0x45, 0x30, 0xC0, + 0xA4, 0x26, 0x8B, 0x3F, 0xA6, 0x3B, 0x1B, 0x60, 0x6F, 0x2D, + 0x26, 0x4A, 0x2D, 0x85, 0x7B, 0xE8, 0xA0, 0x9C, 0x1D, 0xFD, + 0x57, 0x0D, 0x15, 0x85, 0x8B, 0xD4, 0x81, 0x01, 0x04}}; + +/** Set the "FinishAfter" time tag on a JTx */ +auto const finish_time = JTxFieldWrapper(sfFinishAfter); + +/** Set the "CancelAfter" time tag on a JTx */ +auto const cancel_time = JTxFieldWrapper(sfCancelAfter); + +auto const condition = JTxFieldWrapper(sfCondition); + +auto const fulfillment = JTxFieldWrapper(sfFulfillment); + +} // namespace escrow + +} // namespace jtx + +} // namespace test +} // namespace ripple + +#endif diff --git a/src/test/jtx/flags.h b/src/test/jtx/flags.h index 4adc75c6a8..8d3fa4f25c 100644 --- a/src/test/jtx/flags.h +++ b/src/test/jtx/flags.h @@ -80,6 +80,9 @@ private: case asfDisallowIncomingTrustline: mask_ |= lsfDisallowIncomingTrustline; break; + case asfAllowTrustLineLocking: + mask_ |= lsfAllowTrustLineLocking; + break; default: Throw("unknown flag"); } diff --git a/src/test/jtx/impl/AMM.cpp b/src/test/jtx/impl/AMM.cpp index 3482e7e867..90d0f0eacc 100644 --- a/src/test/jtx/impl/AMM.cpp +++ b/src/test/jtx/impl/AMM.cpp @@ -20,6 +20,7 @@ #include #include +#include #include #include @@ -39,12 +40,16 @@ number(STAmount const& a) return a; } -static IOUAmount -initialTokens(STAmount const& asset1, STAmount const& asset2) +IOUAmount +AMM::initialTokens() { - auto const product = number(asset1) * number(asset2); - return (IOUAmount)(product.mantissa() >= 0 ? root2(product) - : root2(-product)); + if (!env_.enabled(fixAMMv1_3)) + { + auto const product = number(asset1_) * number(asset2_); + return (IOUAmount)(product.mantissa() >= 0 ? root2(product) + : root2(-product)); + } + return getLPTokensBalance(); } AMM::AMM( @@ -65,7 +70,6 @@ AMM::AMM( , asset1_(asset1) , asset2_(asset2) , ammID_(keylet::amm(asset1_.issue(), asset2_.issue()).key) - , initialLPTokens_(initialTokens(asset1, asset2)) , log_(log) , doClose_(close) , lastPurchasePrice_(0) @@ -74,10 +78,12 @@ AMM::AMM( , msig_(ms) , fee_(fee) , ammAccount_(create(tfee, flags, seq, ter)) - , lptIssue_(ripple::ammLPTIssue( - asset1_.issue().currency, - asset2_.issue().currency, - ammAccount_)) + , lptIssue_( + ripple::ammLPTIssue( + asset1_.issue().currency, + asset2_.issue().currency, + ammAccount_)) + , initialLPTokens_(initialTokens()) { } @@ -821,7 +827,6 @@ pay(Account const& account, AccountID const& to, STAmount const& amount) jv[jss::Amount] = amount.getJson(JsonOptions::none); jv[jss::Destination] = to_string(to); jv[jss::TransactionType] = jss::Payment; - jv[jss::Flags] = tfUniversal; return jv; } diff --git a/src/test/jtx/impl/AMMTest.cpp b/src/test/jtx/impl/AMMTest.cpp index 8555be01a9..5bb8f14cbf 100644 --- a/src/test/jtx/impl/AMMTest.cpp +++ b/src/test/jtx/impl/AMMTest.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include @@ -105,15 +106,31 @@ AMMTestBase::testAMM( std::uint16_t tfee, std::optional const& ter, std::vector const& vfeatures) +{ + testAMM( + std::move(cb), + TestAMMArg{ + .pool = pool, .tfee = tfee, .ter = ter, .features = vfeatures}); +} + +void +AMMTestBase::testAMM( + std::function&& cb, + TestAMMArg const& arg) { using namespace jtx; - for (auto const& features : vfeatures) + std::string logs; + + for (auto const& features : arg.features) { - Env env{*this, features}; + Env env{ + *this, + features, + arg.noLog ? std::make_unique(&logs) : nullptr}; auto const [asset1, asset2] = - pool ? *pool : std::make_pair(XRP(10000), USD(10000)); + arg.pool ? *arg.pool : std::make_pair(XRP(10000), USD(10000)); auto tofund = [&](STAmount const& a) -> STAmount { if (a.native()) { @@ -143,7 +160,7 @@ AMMTestBase::testAMM( alice, asset1, asset2, - CreateArg{.log = false, .tfee = tfee, .err = ter}); + CreateArg{.log = false, .tfee = arg.tfee, .err = arg.ter}); if (BEAST_EXPECT( ammAlice.expectBalances(asset1, asset2, ammAlice.tokens()))) cb(ammAlice, env); diff --git a/src/test/jtx/impl/Env.cpp b/src/test/jtx/impl/Env.cpp index 96b63bd927..a68c1a74c6 100644 --- a/src/test/jtx/impl/Env.cpp +++ b/src/test/jtx/impl/Env.cpp @@ -96,7 +96,7 @@ Env::AppBundle::~AppBundle() if (app) { app->getJobQueue().rendezvous(); - app->signalStop(); + app->signalStop("~AppBundle"); } if (thread.joinable()) thread.join(); @@ -202,13 +202,31 @@ Env::balance(Account const& account, Issue const& issue) const PrettyAmount Env::balance(Account const& account, MPTIssue const& mptIssue) const { - auto const sle = le(keylet::mptoken(mptIssue.getMptID(), account)); - if (!sle) - { + MPTID const id = mptIssue.getMptID(); + if (!id) return {STAmount(mptIssue, 0), account.name()}; + + AccountID const issuer = mptIssue.getIssuer(); + if (account.id() == issuer) + { + // Issuer balance + auto const sle = le(keylet::mptIssuance(id)); + if (!sle) + return {STAmount(mptIssue, 0), account.name()}; + + STAmount const amount{mptIssue, sle->getFieldU64(sfOutstandingAmount)}; + return {amount, lookup(issuer).name()}; + } + else + { + // Holder balance + auto const sle = le(keylet::mptoken(id, account)); + if (!sle) + return {STAmount(mptIssue, 0), account.name()}; + + STAmount const amount{mptIssue, sle->getFieldU64(sfMPTAmount)}; + return {amount, lookup(issuer).name()}; } - STAmount const amount{mptIssue, sle->getFieldU64(sfMPTAmount)}; - return {amount, lookup(mptIssue.getIssuer()).name()}; } PrettyAmount @@ -219,6 +237,18 @@ Env::balance(Account const& account, Asset const& asset) const asset.value()); } +PrettyAmount +Env::limit(Account const& account, Issue const& issue) const +{ + auto const sle = le(keylet::line(account.id(), issue)); + if (!sle) + return {STAmount(issue, 0), account.name()}; + auto const aHigh = account.id() > issue.account; + if (sle && sle->isFieldPresent(aHigh ? sfLowLimit : sfHighLimit)) + return {(*sle)[aHigh ? sfLowLimit : sfHighLimit], account.name()}; + return {STAmount(issue, 0), account.name()}; +} + std::uint32_t Env::ownerCount(Account const& account) const { @@ -466,7 +496,12 @@ Env::postconditions( std::shared_ptr Env::meta() { - close(); + if (current()->txCount() != 0) + { + // close the ledger if it has not already been closed + // (metadata is not finalized until the ledger is closed) + close(); + } auto const item = closed()->txRead(txid_); return item.second; } diff --git a/src/test/jtx/impl/TestHelpers.cpp b/src/test/jtx/impl/TestHelpers.cpp index f5f0368d47..192c48c26b 100644 --- a/src/test/jtx/impl/TestHelpers.cpp +++ b/src/test/jtx/impl/TestHelpers.cpp @@ -231,45 +231,6 @@ expectLedgerEntryRoot( return accountBalance(env, acct) == to_string(expectedValue.xrp()); } -/* Escrow */ -/******************************************************************************/ - -Json::Value -escrow(AccountID const& account, AccountID const& to, STAmount const& amount) -{ - Json::Value jv; - jv[jss::TransactionType] = jss::EscrowCreate; - jv[jss::Flags] = tfUniversal; - jv[jss::Account] = to_string(account); - jv[jss::Destination] = to_string(to); - jv[jss::Amount] = amount.getJson(JsonOptions::none); - return jv; -} - -Json::Value -finish(AccountID const& account, AccountID const& from, std::uint32_t seq) -{ - Json::Value jv; - jv[jss::TransactionType] = jss::EscrowFinish; - jv[jss::Flags] = tfUniversal; - jv[jss::Account] = to_string(account); - jv[sfOwner.jsonName] = to_string(from); - jv[sfOfferSequence.jsonName] = seq; - return jv; -} - -Json::Value -cancel(AccountID const& account, Account const& from, std::uint32_t seq) -{ - Json::Value jv; - jv[jss::TransactionType] = jss::EscrowCancel; - jv[jss::Flags] = tfUniversal; - jv[jss::Account] = to_string(account); - jv[sfOwner.jsonName] = from.human(); - jv[sfOfferSequence.jsonName] = seq; - return jv; -} - /* Payment Channel */ /******************************************************************************/ Json::Value @@ -284,7 +245,6 @@ create( { Json::Value jv; jv[jss::TransactionType] = jss::PaymentChannelCreate; - jv[jss::Flags] = tfUniversal; jv[jss::Account] = to_string(account); jv[jss::Destination] = to_string(to); jv[jss::Amount] = amount.getJson(JsonOptions::none); @@ -306,7 +266,6 @@ fund( { Json::Value jv; jv[jss::TransactionType] = jss::PaymentChannelFund; - jv[jss::Flags] = tfUniversal; jv[jss::Account] = to_string(account); jv[sfChannel.fieldName] = to_string(channel); jv[jss::Amount] = amount.getJson(JsonOptions::none); @@ -326,7 +285,6 @@ claim( { Json::Value jv; jv[jss::TransactionType] = jss::PaymentChannelClaim; - jv[jss::Flags] = tfUniversal; jv[jss::Account] = to_string(account); jv["Channel"] = to_string(channel); if (amount) diff --git a/src/test/jtx/impl/acctdelete.cpp b/src/test/jtx/impl/acctdelete.cpp index 842eea7fc2..acce912d46 100644 --- a/src/test/jtx/impl/acctdelete.cpp +++ b/src/test/jtx/impl/acctdelete.cpp @@ -17,6 +17,7 @@ */ //============================================================================== +#include #include #include @@ -36,6 +37,28 @@ acctdelete(jtx::Account const& account, jtx::Account const& dest) return jv; } +// Close the ledger until the ledger sequence is large enough to close +// the account. If margin is specified, close the ledger so `margin` +// more closes are needed +void +incLgrSeqForAccDel(jtx::Env& env, jtx::Account const& acc, std::uint32_t margin) +{ + using namespace jtx; + auto openLedgerSeq = [](jtx::Env& env) -> std::uint32_t { + return env.current()->seq(); + }; + + int const delta = [&]() -> int { + if (env.seq(acc) + 255 > openLedgerSeq(env)) + return env.seq(acc) - openLedgerSeq(env) + 255 - margin; + return 0; + }(); + env.test.BEAST_EXPECT(margin == 0 || delta >= 0); + for (int i = 0; i < delta; ++i) + env.close(); + env.test.BEAST_EXPECT(openLedgerSeq(env) == env.seq(acc) + 255 - margin); +} + } // namespace jtx } // namespace test } // namespace ripple diff --git a/src/test/jtx/impl/batch.cpp b/src/test/jtx/impl/batch.cpp new file mode 100644 index 0000000000..055ed3fb55 --- /dev/null +++ b/src/test/jtx/impl/batch.cpp @@ -0,0 +1,154 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include + +#include +#include +#include +#include + +#include +#include + +namespace ripple { +namespace test { +namespace jtx { + +namespace batch { + +XRPAmount +calcBatchFee( + test::jtx::Env const& env, + uint32_t const& numSigners, + uint32_t const& txns) +{ + XRPAmount const feeDrops = env.current()->fees().base; + return ((numSigners + 2) * feeDrops) + feeDrops * txns; +} + +// Batch. +Json::Value +outer( + jtx::Account const& account, + uint32_t seq, + STAmount const& fee, + std::uint32_t flags) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::Batch; + jv[jss::Account] = account.human(); + jv[jss::RawTransactions] = Json::Value{Json::arrayValue}; + jv[jss::Sequence] = seq; + jv[jss::Flags] = flags; + jv[jss::Fee] = to_string(fee); + return jv; +} + +void +inner::operator()(Env& env, JTx& jt) const +{ + auto const index = jt.jv[jss::RawTransactions].size(); + Json::Value& batchTransaction = jt.jv[jss::RawTransactions][index]; + + // Initialize the batch transaction + batchTransaction = Json::Value{}; + batchTransaction[jss::RawTransaction] = txn_; +} + +void +sig::operator()(Env& env, JTx& jt) const +{ + auto const mySigners = signers; + std::optional st; + try + { + // required to cast the STObject to STTx + jt.jv[jss::SigningPubKey] = ""; + st = parse(jt.jv); + } + catch (parse_error const&) + { + env.test.log << pretty(jt.jv) << std::endl; + Rethrow(); + } + STTx const& stx = STTx{std::move(*st)}; + auto& js = jt[sfBatchSigners.getJsonName()]; + for (std::size_t i = 0; i < mySigners.size(); ++i) + { + auto const& e = mySigners[i]; + auto& jo = js[i][sfBatchSigner.getJsonName()]; + jo[jss::Account] = e.acct.human(); + jo[jss::SigningPubKey] = strHex(e.sig.pk().slice()); + + Serializer msg; + serializeBatch(msg, stx.getFlags(), stx.getBatchTransactionIDs()); + auto const sig = ripple::sign( + *publicKeyType(e.sig.pk().slice()), e.sig.sk(), msg.slice()); + jo[sfTxnSignature.getJsonName()] = + strHex(Slice{sig.data(), sig.size()}); + } +} + +void +msig::operator()(Env& env, JTx& jt) const +{ + auto const mySigners = signers; + std::optional st; + try + { + // required to cast the STObject to STTx + jt.jv[jss::SigningPubKey] = ""; + st = parse(jt.jv); + } + catch (parse_error const&) + { + env.test.log << pretty(jt.jv) << std::endl; + Rethrow(); + } + STTx const& stx = STTx{std::move(*st)}; + auto& bs = jt[sfBatchSigners.getJsonName()]; + auto const index = jt[sfBatchSigners.jsonName].size(); + auto& bso = bs[index][sfBatchSigner.getJsonName()]; + bso[jss::Account] = master.human(); + bso[jss::SigningPubKey] = ""; + auto& is = bso[sfSigners.getJsonName()]; + for (std::size_t i = 0; i < mySigners.size(); ++i) + { + auto const& e = mySigners[i]; + auto& iso = is[i][sfSigner.getJsonName()]; + iso[jss::Account] = e.acct.human(); + iso[jss::SigningPubKey] = strHex(e.sig.pk().slice()); + + Serializer msg; + serializeBatch(msg, stx.getFlags(), stx.getBatchTransactionIDs()); + finishMultiSigningData(e.acct.id(), msg); + auto const sig = ripple::sign( + *publicKeyType(e.sig.pk().slice()), e.sig.sk(), msg.slice()); + iso[sfTxnSignature.getJsonName()] = + strHex(Slice{sig.data(), sig.size()}); + } +} + +} // namespace batch + +} // namespace jtx +} // namespace test +} // namespace ripple diff --git a/src/test/jtx/impl/check.cpp b/src/test/jtx/impl/check.cpp index f5aa76658c..831bc900e7 100644 --- a/src/test/jtx/impl/check.cpp +++ b/src/test/jtx/impl/check.cpp @@ -37,7 +37,6 @@ cash(jtx::Account const& dest, uint256 const& checkId, STAmount const& amount) jv[sfAmount.jsonName] = amount.getJson(JsonOptions::none); jv[sfCheckID.jsonName] = to_string(checkId); jv[sfTransactionType.jsonName] = jss::CheckCash; - jv[sfFlags.jsonName] = tfUniversal; return jv; } @@ -53,7 +52,6 @@ cash( jv[sfDeliverMin.jsonName] = atLeast.value.getJson(JsonOptions::none); jv[sfCheckID.jsonName] = to_string(checkId); jv[sfTransactionType.jsonName] = jss::CheckCash; - jv[sfFlags.jsonName] = tfUniversal; return jv; } @@ -65,7 +63,6 @@ cancel(jtx::Account const& dest, uint256 const& checkId) jv[sfAccount.jsonName] = dest.human(); jv[sfCheckID.jsonName] = to_string(checkId); jv[sfTransactionType.jsonName] = jss::CheckCancel; - jv[sfFlags.jsonName] = tfUniversal; return jv; } diff --git a/src/test/jtx/impl/creds.cpp b/src/test/jtx/impl/creds.cpp index f29bc45e20..eae3b9501b 100644 --- a/src/test/jtx/impl/creds.cpp +++ b/src/test/jtx/impl/creds.cpp @@ -39,8 +39,6 @@ create( jv[jss::Account] = issuer.human(); jv[jss::Subject] = subject.human(); - - jv[jss::Flags] = tfUniversal; jv[sfCredentialType.jsonName] = strHex(credType); return jv; @@ -57,8 +55,6 @@ accept( jv[jss::Account] = subject.human(); jv[jss::Issuer] = issuer.human(); jv[sfCredentialType.jsonName] = strHex(credType); - jv[jss::Flags] = tfUniversal; - return jv; } @@ -75,7 +71,6 @@ deleteCred( jv[jss::Subject] = subject.human(); jv[jss::Issuer] = issuer.human(); jv[sfCredentialType.jsonName] = strHex(credType); - jv[jss::Flags] = tfUniversal; return jv; } diff --git a/src/test/jtx/impl/dids.cpp b/src/test/jtx/impl/dids.cpp index 67a523403c..1b443a5d9d 100644 --- a/src/test/jtx/impl/dids.cpp +++ b/src/test/jtx/impl/dids.cpp @@ -35,7 +35,6 @@ set(jtx::Account const& account) Json::Value jv; jv[jss::TransactionType] = jss::DIDSet; jv[jss::Account] = to_string(account.id()); - jv[jss::Flags] = tfUniversal; return jv; } @@ -45,7 +44,6 @@ setValid(jtx::Account const& account) Json::Value jv; jv[jss::TransactionType] = jss::DIDSet; jv[jss::Account] = to_string(account.id()); - jv[jss::Flags] = tfUniversal; jv[sfURI.jsonName] = strHex(std::string{"uri"}); return jv; } @@ -56,7 +54,6 @@ del(jtx::Account const& account) Json::Value jv; jv[jss::TransactionType] = jss::DIDDelete; jv[jss::Account] = to_string(account.id()); - jv[jss::Flags] = tfUniversal; return jv; } diff --git a/src/test/jtx/impl/domain.cpp b/src/test/jtx/impl/domain.cpp new file mode 100644 index 0000000000..51adb4ce98 --- /dev/null +++ b/src/test/jtx/impl/domain.cpp @@ -0,0 +1,36 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +#include + +namespace ripple { +namespace test { +namespace jtx { + +void +domain::operator()(Env&, JTx& jt) const +{ + jt[sfDomainID.jsonName] = to_string(v_); +} + +} // namespace jtx +} // namespace test +} // namespace ripple diff --git a/src/test/jtx/impl/envconfig.cpp b/src/test/jtx/impl/envconfig.cpp index dd9c735465..624036196d 100644 --- a/src/test/jtx/impl/envconfig.cpp +++ b/src/test/jtx/impl/envconfig.cpp @@ -140,6 +140,39 @@ addGrpcConfigWithSecureGateway( return cfg; } +std::unique_ptr +makeConfig( + std::map extraTxQ, + std::map extraVoting) +{ + auto p = test::jtx::envconfig(); + auto& section = p->section("transaction_queue"); + section.set("ledgers_in_queue", "2"); + section.set("minimum_queue_size", "2"); + section.set("min_ledgers_to_compute_size_limit", "3"); + section.set("max_ledger_counts_to_store", "100"); + section.set("retry_sequence_percent", "25"); + section.set("normal_consensus_increase_percent", "0"); + + for (auto const& [k, v] : extraTxQ) + section.set(k, v); + + // Some tests specify different fee settings that are enabled by + // a FeeVote + if (!extraVoting.empty()) + { + auto& votingSection = p->section("voting"); + for (auto const& [k, v] : extraVoting) + { + votingSection.set(k, v); + } + + // In order for the vote to occur, we must run as a validator + p->section("validation_seed").legacy("shUwVw52ofnCUX5m7kPTKzJdr4HEH"); + } + return p; +} + } // namespace jtx } // namespace test } // namespace ripple diff --git a/src/test/jtx/impl/escrow.cpp b/src/test/jtx/impl/escrow.cpp new file mode 100644 index 0000000000..a1ec6a3c5e --- /dev/null +++ b/src/test/jtx/impl/escrow.cpp @@ -0,0 +1,82 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2019 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +#include +#include + +namespace ripple { +namespace test { +namespace jtx { + +/** Escrow operations. */ +namespace escrow { + +Json::Value +create(AccountID const& account, AccountID const& to, STAmount const& amount) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::EscrowCreate; + jv[jss::Flags] = tfFullyCanonicalSig; + jv[jss::Account] = to_string(account); + jv[jss::Destination] = to_string(to); + jv[jss::Amount] = amount.getJson(JsonOptions::none); + return jv; +} + +Json::Value +finish(AccountID const& account, AccountID const& from, std::uint32_t seq) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::EscrowFinish; + jv[jss::Flags] = tfFullyCanonicalSig; + jv[jss::Account] = to_string(account); + jv[sfOwner.jsonName] = to_string(from); + jv[sfOfferSequence.jsonName] = seq; + return jv; +} + +Json::Value +cancel(AccountID const& account, Account const& from, std::uint32_t seq) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::EscrowCancel; + jv[jss::Flags] = tfFullyCanonicalSig; + jv[jss::Account] = to_string(account); + jv[sfOwner.jsonName] = from.human(); + jv[sfOfferSequence.jsonName] = seq; + return jv; +} + +Rate +rate(Env& env, Account const& account, std::uint32_t const& seq) +{ + auto const sle = env.le(keylet::escrow(account.id(), seq)); + if (sle->isFieldPresent(sfTransferRate)) + return ripple::Rate((*sle)[sfTransferRate]); + return Rate{0}; +} + +} // namespace escrow + +} // namespace jtx + +} // namespace test +} // namespace ripple diff --git a/src/test/jtx/impl/ledgerStateFixes.cpp b/src/test/jtx/impl/ledgerStateFixes.cpp index 8c78069191..b7df78dd11 100644 --- a/src/test/jtx/impl/ledgerStateFixes.cpp +++ b/src/test/jtx/impl/ledgerStateFixes.cpp @@ -39,7 +39,6 @@ nftPageLinks(jtx::Account const& acct, jtx::Account const& owner) jv[sfLedgerFixType.jsonName] = LedgerStateFix::nfTokenPageLink; jv[sfOwner.jsonName] = owner.human(); jv[sfTransactionType.jsonName] = jss::LedgerStateFix; - jv[sfFlags.jsonName] = tfUniversal; return jv; } diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index c8ff167221..d33432d316 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -17,7 +17,7 @@ */ //============================================================================== -#include +#include #include diff --git a/src/test/jtx/impl/multisign.cpp b/src/test/jtx/impl/multisign.cpp index a802528247..6ed6df6804 100644 --- a/src/test/jtx/impl/multisign.cpp +++ b/src/test/jtx/impl/multisign.cpp @@ -65,17 +65,6 @@ signers(Account const& account, none_t) //------------------------------------------------------------------------------ -msig::msig(std::vector signers_) : signers(std::move(signers_)) -{ - // Signatures must be applied in sorted order. - std::sort( - signers.begin(), - signers.end(), - [](msig::Reg const& lhs, msig::Reg const& rhs) { - return lhs.acct.id() < rhs.acct.id(); - }); -} - void msig::operator()(Env& env, JTx& jt) const { diff --git a/src/test/jtx/impl/paths.cpp b/src/test/jtx/impl/paths.cpp index 2a45909eb9..f230305469 100644 --- a/src/test/jtx/impl/paths.cpp +++ b/src/test/jtx/impl/paths.cpp @@ -23,6 +23,8 @@ #include +#include + namespace ripple { namespace test { namespace jtx { @@ -34,6 +36,18 @@ paths::operator()(Env& env, JTx& jt) const auto const from = env.lookup(jv[jss::Account].asString()); auto const to = env.lookup(jv[jss::Destination].asString()); auto const amount = amountFromJson(sfAmount, jv[jss::Amount]); + + std::optional domain; + if (jv.isMember(sfDomainID.jsonName)) + { + if (!jv[sfDomainID.jsonName].isString()) + return; + uint256 num; + auto const s = jv[sfDomainID.jsonName].asString(); + if (num.parseHex(s)) + domain = num; + } + Pathfinder pf( std::make_shared( env.current(), env.app().journal("RippleLineCache")), @@ -43,6 +57,7 @@ paths::operator()(Env& env, JTx& jt) const in_.account, amount, std::nullopt, + domain, env.app()); if (!pf.findPaths(depth_)) return; diff --git a/src/test/jtx/impl/pay.cpp b/src/test/jtx/impl/pay.cpp index 82fe910e9b..d1d994059e 100644 --- a/src/test/jtx/impl/pay.cpp +++ b/src/test/jtx/impl/pay.cpp @@ -35,7 +35,7 @@ pay(AccountID const& account, AccountID const& to, AnyAmount amount) jv[jss::Amount] = amount.value.getJson(JsonOptions::none); jv[jss::Destination] = to_string(to); jv[jss::TransactionType] = jss::Payment; - jv[jss::Flags] = tfUniversal; + jv[jss::Flags] = tfFullyCanonicalSig; return jv; } Json::Value diff --git a/src/test/jtx/impl/permissioned_dex.cpp b/src/test/jtx/impl/permissioned_dex.cpp new file mode 100644 index 0000000000..4b09a11880 --- /dev/null +++ b/src/test/jtx/impl/permissioned_dex.cpp @@ -0,0 +1,85 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +#include +#include + +#include + +namespace ripple { +namespace test { +namespace jtx { + +uint256 +setupDomain( + jtx::Env& env, + std::vector const& accounts, + jtx::Account const& domainOwner, + std::string const& credType) +{ + using namespace jtx; + env.fund(XRP(100000), domainOwner); + env.close(); + + pdomain::Credentials credentials{{domainOwner, credType}}; + env(pdomain::setTx(domainOwner, credentials)); + + auto const objects = pdomain::getObjects(domainOwner, env); + auto const domainID = objects.begin()->first; + + for (auto const& account : accounts) + { + env(credentials::create(account, domainOwner, credType)); + env.close(); + env(credentials::accept(account, domainOwner, credType)); + env.close(); + } + return domainID; +} + +PermissionedDEX::PermissionedDEX(Env& env) + : gw("permdex-gateway") + , domainOwner("permdex-domainOwner") + , alice("permdex-alice") + , bob("permdex-bob") + , carol("permdex-carol") + , USD(gw["USD"]) + , credType("permdex-abcde") +{ + // Fund accounts + env.fund(XRP(100000), alice, bob, carol, gw); + env.close(); + + domainID = setupDomain(env, {alice, bob, carol, gw}, domainOwner, credType); + + for (auto const& account : {alice, bob, carol, domainOwner}) + { + env.trust(USD(1000), account); + env.close(); + + env(pay(gw, account, USD(100))); + env.close(); + } +} + +} // namespace jtx +} // namespace test +} // namespace ripple diff --git a/src/test/jtx/impl/permissioned_domains.cpp b/src/test/jtx/impl/permissioned_domains.cpp index 866ca3bb7e..441ee325c8 100644 --- a/src/test/jtx/impl/permissioned_domains.cpp +++ b/src/test/jtx/impl/permissioned_domains.cpp @@ -17,7 +17,7 @@ */ //============================================================================== -#include +#include namespace ripple { namespace test { diff --git a/src/test/jtx/impl/txflags.cpp b/src/test/jtx/impl/txflags.cpp index 77c46f35b3..12c9cfeb83 100644 --- a/src/test/jtx/impl/txflags.cpp +++ b/src/test/jtx/impl/txflags.cpp @@ -28,7 +28,7 @@ namespace jtx { void txflags::operator()(Env&, JTx& jt) const { - jt[jss::Flags] = v_ /*| tfUniversal*/; + jt[jss::Flags] = v_ /*| tfFullyCanonicalSig*/; } } // namespace jtx diff --git a/src/test/jtx/impl/xchain_bridge.cpp b/src/test/jtx/impl/xchain_bridge.cpp index c63734ee8f..6f167d7508 100644 --- a/src/test/jtx/impl/xchain_bridge.cpp +++ b/src/test/jtx/impl/xchain_bridge.cpp @@ -84,7 +84,6 @@ bridge_create( minAccountCreate->getJson(JsonOptions::none); jv[jss::TransactionType] = jss::XChainCreateBridge; - jv[jss::Flags] = tfUniversal; return jv; } @@ -107,7 +106,6 @@ bridge_modify( minAccountCreate->getJson(JsonOptions::none); jv[jss::TransactionType] = jss::XChainModifyBridge; - jv[jss::Flags] = tfUniversal; return jv; } @@ -126,7 +124,6 @@ xchain_create_claim_id( jv[sfOtherChainSource.getJsonName()] = otherChainSource.human(); jv[jss::TransactionType] = jss::XChainCreateClaimID; - jv[jss::Flags] = tfUniversal; return jv; } @@ -148,7 +145,6 @@ xchain_commit( jv[sfOtherChainDestination.getJsonName()] = dst->human(); jv[jss::TransactionType] = jss::XChainCommit; - jv[jss::Flags] = tfUniversal; return jv; } @@ -169,7 +165,6 @@ xchain_claim( jv[sfAmount.getJsonName()] = amt.value.getJson(JsonOptions::none); jv[jss::TransactionType] = jss::XChainClaim; - jv[jss::Flags] = tfUniversal; return jv; } @@ -191,7 +186,6 @@ sidechain_xchain_account_create( reward.value.getJson(JsonOptions::none); jv[jss::TransactionType] = jss::XChainAccountCreateCommit; - jv[jss::Flags] = tfUniversal; return jv; } @@ -242,7 +236,6 @@ claim_attestation( result[sfDestination.getJsonName()] = toBase58(*dst); result[jss::TransactionType] = jss::XChainAddClaimAttestation; - result[jss::Flags] = tfUniversal; return result; } @@ -297,7 +290,6 @@ create_account_attestation( rewardAmount.value.getJson(JsonOptions::none); result[jss::TransactionType] = jss::XChainAddAccountCreateAttestation; - result[jss::Flags] = tfUniversal; return result; } @@ -397,7 +389,7 @@ XChainBridgeObjects::XChainBridgeObjects() bridge_rpc(mcDoor, xrpIssue(), Account::master, xrpIssue())) , jvb(bridge(mcDoor, xrpIssue(), Account::master, xrpIssue())) , jvub(bridge(mcuDoor, xrpIssue(), Account::master, xrpIssue())) - , features(supported_amendments() | FeatureBitset{featureXChainBridge}) + , features(testable_amendments() | FeatureBitset{featureXChainBridge}) , signers([] { constexpr int numSigners = UT_XCHAIN_DEFAULT_NUM_SIGNERS; std::vector result; diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h index 52ade92323..64eaa452f5 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -20,7 +20,8 @@ #ifndef RIPPLE_TEST_JTX_MPT_H_INCLUDED #define RIPPLE_TEST_JTX_MPT_H_INCLUDED -#include +#include +#include #include #include diff --git a/src/test/jtx/multisign.h b/src/test/jtx/multisign.h index 6bcb1a671c..1fed895c6d 100644 --- a/src/test/jtx/multisign.h +++ b/src/test/jtx/multisign.h @@ -21,6 +21,7 @@ #define RIPPLE_TEST_JTX_MULTISIGN_H_INCLUDED #include +#include #include #include #include @@ -65,48 +66,19 @@ signers(Account const& account, none_t); class msig { public: - struct Reg - { - Account acct; - Account sig; - - Reg(Account const& masterSig) : acct(masterSig), sig(masterSig) - { - } - - Reg(Account const& acct_, Account const& regularSig) - : acct(acct_), sig(regularSig) - { - } - - Reg(char const* masterSig) : acct(masterSig), sig(masterSig) - { - } - - Reg(char const* acct_, char const* regularSig) - : acct(acct_), sig(regularSig) - { - } - - bool - operator<(Reg const& rhs) const - { - return acct < rhs.acct; - } - }; - std::vector signers; -public: - msig(std::vector signers_); + msig(std::vector signers_) : signers(std::move(signers_)) + { + sortSigners(signers); + } template requires std::convertible_to explicit msig(AccountType&& a0, Accounts&&... aN) - : msig{std::vector{ - std::forward(a0), - std::forward(aN)...}} + : signers{std::forward(a0), std::forward(aN)...} { + sortSigners(signers); } void diff --git a/src/test/jtx/permissioned_dex.h b/src/test/jtx/permissioned_dex.h new file mode 100644 index 0000000000..b95574d94d --- /dev/null +++ b/src/test/jtx/permissioned_dex.h @@ -0,0 +1,53 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#include +#include + +namespace ripple { +namespace test { +namespace jtx { + +uint256 +setupDomain( + jtx::Env& env, + std::vector const& accounts, + jtx::Account const& domainOwner = jtx::Account("domainOwner"), + std::string const& credType = "Cred"); + +class PermissionedDEX +{ +public: + Account gw; + Account domainOwner; + Account alice; + Account bob; + Account carol; + IOU USD; + uint256 domainID; + std::string credType; + + PermissionedDEX(Env& env); +}; + +} // namespace jtx +} // namespace test +} // namespace ripple diff --git a/src/test/jtx/permissioned_domains.h b/src/test/jtx/permissioned_domains.h index ee80c6a69f..ed086e366d 100644 --- a/src/test/jtx/permissioned_domains.h +++ b/src/test/jtx/permissioned_domains.h @@ -20,7 +20,8 @@ #ifndef RIPPLE_TEST_JTX_PERMISSIONED_DOMAINS_H_INCLUDED #define RIPPLE_TEST_JTX_PERMISSIONED_DOMAINS_H_INCLUDED -#include +#include +#include #include namespace ripple { diff --git a/src/test/ledger/BookDirs_test.cpp b/src/test/ledger/BookDirs_test.cpp index ed7ca91083..52b618e9a0 100644 --- a/src/test/ledger/BookDirs_test.cpp +++ b/src/test/ledger/BookDirs_test.cpp @@ -37,7 +37,7 @@ struct BookDirs_test : public beast::unit_test::suite env.close(); { - Book book(xrpIssue(), USD.issue()); + Book book(xrpIssue(), USD.issue(), std::nullopt); { auto d = BookDirs(*env.current(), book); BEAST_EXPECT(std::begin(d) == std::end(d)); @@ -53,14 +53,16 @@ struct BookDirs_test : public beast::unit_test::suite env(offer("alice", Account("alice")["USD"](50), XRP(10))); auto d = BookDirs( *env.current(), - Book(Account("alice")["USD"].issue(), xrpIssue())); + Book( + Account("alice")["USD"].issue(), xrpIssue(), std::nullopt)); BEAST_EXPECT(std::distance(d.begin(), d.end()) == 1); } { env(offer("alice", gw["CNY"](50), XRP(10))); - auto d = - BookDirs(*env.current(), Book(gw["CNY"].issue(), xrpIssue())); + auto d = BookDirs( + *env.current(), + Book(gw["CNY"].issue(), xrpIssue(), std::nullopt)); BEAST_EXPECT(std::distance(d.begin(), d.end()) == 1); } @@ -70,7 +72,7 @@ struct BookDirs_test : public beast::unit_test::suite env(offer("alice", USD(50), Account("bob")["CNY"](10))); auto d = BookDirs( *env.current(), - Book(USD.issue(), Account("bob")["CNY"].issue())); + Book(USD.issue(), Account("bob")["CNY"].issue(), std::nullopt)); BEAST_EXPECT(std::distance(d.begin(), d.end()) == 1); } @@ -80,7 +82,8 @@ struct BookDirs_test : public beast::unit_test::suite for (auto k = 0; k < 80; ++k) env(offer("alice", AUD(i), XRP(j))); - auto d = BookDirs(*env.current(), Book(AUD.issue(), xrpIssue())); + auto d = BookDirs( + *env.current(), Book(AUD.issue(), xrpIssue(), std::nullopt)); BEAST_EXPECT(std::distance(d.begin(), d.end()) == 240); auto i = 1, j = 3, k = 0; for (auto const& e : d) @@ -100,8 +103,8 @@ struct BookDirs_test : public beast::unit_test::suite run() override { using namespace jtx; - auto const sa = supported_amendments(); - test_bookdir(sa - featureFlowCross); + auto const sa = testable_amendments(); + test_bookdir(sa - featurePermissionedDEX); test_bookdir(sa); } }; diff --git a/src/test/ledger/Directory_test.cpp b/src/test/ledger/Directory_test.cpp index 825d7ff340..9e8d40e0cc 100644 --- a/src/test/ledger/Directory_test.cpp +++ b/src/test/ledger/Directory_test.cpp @@ -132,7 +132,8 @@ struct Directory_test : public beast::unit_test::suite // Now check the orderbook: it should be in the order we placed // the offers. - auto book = BookDirs(*env.current(), Book({xrpIssue(), USD.issue()})); + auto book = BookDirs( + *env.current(), Book({xrpIssue(), USD.issue(), std::nullopt})); int count = 1; for (auto const& offer : book) @@ -291,7 +292,8 @@ struct Directory_test : public beast::unit_test::suite // should have no entries and be empty: { Sandbox sb(env.closed().get(), tapNONE); - uint256 const bookBase = getBookBase({xrpIssue(), USD.issue()}); + uint256 const bookBase = + getBookBase({xrpIssue(), USD.issue(), std::nullopt}); BEAST_EXPECT(dirIsEmpty(sb, keylet::page(bookBase))); BEAST_EXPECT(!sb.succ(bookBase, getQualityNext(bookBase))); @@ -419,7 +421,7 @@ struct Directory_test : public beast::unit_test::suite }; // fixPreviousTxnID is disabled. - Env env(*this, supported_amendments() - fixPreviousTxnID); + Env env(*this, testable_amendments() - fixPreviousTxnID); env.fund(XRP(10000), alice, gw); env.close(); env.trust(USD(1000), alice); diff --git a/src/test/ledger/Invariants_test.cpp b/src/test/ledger/Invariants_test.cpp index 5bb9feb070..133b45b939 100644 --- a/src/test/ledger/Invariants_test.cpp +++ b/src/test/ledger/Invariants_test.cpp @@ -79,7 +79,7 @@ class Invariants_test : public beast::unit_test::suite Preclose const& preclose = {}) { using namespace test::jtx; - FeatureBitset amendments = supported_amendments() | + FeatureBitset amendments = testable_amendments() | featureInvariantsV1_1 | featureSingleAssetVault; Env env{*this, amendments}; @@ -732,21 +732,6 @@ class Invariants_test : public beast::unit_test::suite using namespace test::jtx; testcase << "no zero escrow"; - doInvariantCheck( - {{"Cannot return non-native STAmount as XRPAmount"}}, - [](Account const& A1, Account const& A2, ApplyContext& ac) { - // escrow with nonnative amount - auto const sle = ac.view().peek(keylet::account(A1.id())); - if (!sle) - return false; - auto sleNew = std::make_shared( - keylet::escrow(A1, (*sle)[sfSequence] + 2)); - STAmount nonNative(A2["USD"](51)); - sleNew->setFieldAmount(sfAmount, nonNative); - ac.view().insert(sleNew); - return true; - }); - doInvariantCheck( {{"XRP net change of -1000000 doesn't match fee 0"}, {"escrow specifies invalid amount"}}, @@ -778,6 +763,153 @@ class Invariants_test : public beast::unit_test::suite ac.view().insert(sleNew); return true; }); + + // IOU < 0 + doInvariantCheck( + {{"escrow specifies invalid amount"}}, + [](Account const& A1, Account const&, ApplyContext& ac) { + // escrow with too-little iou + auto const sle = ac.view().peek(keylet::account(A1.id())); + if (!sle) + return false; + auto sleNew = std::make_shared( + keylet::escrow(A1, (*sle)[sfSequence] + 2)); + + Issue const usd{ + Currency(0x5553440000000000), AccountID(0x4985601)}; + STAmount amt(usd, -1); + sleNew->setFieldAmount(sfAmount, amt); + ac.view().insert(sleNew); + return true; + }); + + // IOU bad currency + doInvariantCheck( + {{"escrow specifies invalid amount"}}, + [](Account const& A1, Account const&, ApplyContext& ac) { + // escrow with bad iou currency + auto const sle = ac.view().peek(keylet::account(A1.id())); + if (!sle) + return false; + auto sleNew = std::make_shared( + keylet::escrow(A1, (*sle)[sfSequence] + 2)); + + Issue const bad{badCurrency(), AccountID(0x4985601)}; + STAmount amt(bad, 1); + sleNew->setFieldAmount(sfAmount, amt); + ac.view().insert(sleNew); + return true; + }); + + // MPT < 0 + doInvariantCheck( + {{"escrow specifies invalid amount"}}, + [](Account const& A1, Account const&, ApplyContext& ac) { + // escrow with too-little mpt + auto const sle = ac.view().peek(keylet::account(A1.id())); + if (!sle) + return false; + auto sleNew = std::make_shared( + keylet::escrow(A1, (*sle)[sfSequence] + 2)); + + MPTIssue const mpt{ + MPTIssue{makeMptID(1, AccountID(0x4985601))}}; + STAmount amt(mpt, -1); + sleNew->setFieldAmount(sfAmount, amt); + ac.view().insert(sleNew); + return true; + }); + + // MPT OutstandingAmount < 0 + doInvariantCheck( + {{"escrow specifies invalid amount"}}, + [](Account const& A1, Account const&, ApplyContext& ac) { + // mpissuance outstanding is negative + auto const sle = ac.view().peek(keylet::account(A1.id())); + if (!sle) + return false; + + MPTIssue const mpt{ + MPTIssue{makeMptID(1, AccountID(0x4985601))}}; + auto sleNew = + std::make_shared(keylet::mptIssuance(mpt.getMptID())); + sleNew->setFieldU64(sfOutstandingAmount, -1); + ac.view().insert(sleNew); + return true; + }); + + // MPT LockedAmount < 0 + doInvariantCheck( + {{"escrow specifies invalid amount"}}, + [](Account const& A1, Account const&, ApplyContext& ac) { + // mpissuance locked is less than locked + auto const sle = ac.view().peek(keylet::account(A1.id())); + if (!sle) + return false; + + MPTIssue const mpt{ + MPTIssue{makeMptID(1, AccountID(0x4985601))}}; + auto sleNew = + std::make_shared(keylet::mptIssuance(mpt.getMptID())); + sleNew->setFieldU64(sfLockedAmount, -1); + ac.view().insert(sleNew); + return true; + }); + + // MPT OutstandingAmount < LockedAmount + doInvariantCheck( + {{"escrow specifies invalid amount"}}, + [](Account const& A1, Account const&, ApplyContext& ac) { + // mpissuance outstanding is less than locked + auto const sle = ac.view().peek(keylet::account(A1.id())); + if (!sle) + return false; + + MPTIssue const mpt{ + MPTIssue{makeMptID(1, AccountID(0x4985601))}}; + auto sleNew = + std::make_shared(keylet::mptIssuance(mpt.getMptID())); + sleNew->setFieldU64(sfOutstandingAmount, 1); + sleNew->setFieldU64(sfLockedAmount, 10); + ac.view().insert(sleNew); + return true; + }); + + // MPT MPTAmount < 0 + doInvariantCheck( + {{"escrow specifies invalid amount"}}, + [](Account const& A1, Account const&, ApplyContext& ac) { + // mptoken amount is negative + auto const sle = ac.view().peek(keylet::account(A1.id())); + if (!sle) + return false; + + MPTIssue const mpt{ + MPTIssue{makeMptID(1, AccountID(0x4985601))}}; + auto sleNew = + std::make_shared(keylet::mptoken(mpt.getMptID(), A1)); + sleNew->setFieldU64(sfMPTAmount, -1); + ac.view().insert(sleNew); + return true; + }); + + // MPT LockedAmount < 0 + doInvariantCheck( + {{"escrow specifies invalid amount"}}, + [](Account const& A1, Account const&, ApplyContext& ac) { + // mptoken locked amount is negative + auto const sle = ac.view().peek(keylet::account(A1.id())); + if (!sle) + return false; + + MPTIssue const mpt{ + MPTIssue{makeMptID(1, AccountID(0x4985601))}}; + auto sleNew = + std::make_shared(keylet::mptoken(mpt.getMptID(), A1)); + sleNew->setFieldU64(sfLockedAmount, -1); + ac.view().insert(sleNew); + return true; + }); } void @@ -1047,6 +1179,30 @@ class Invariants_test : public beast::unit_test::suite }); } + void + createPermissionedDomain( + ApplyContext& ac, + std::shared_ptr& sle, + test::jtx::Account const& A1, + test::jtx::Account const& A2) + { + sle->setAccountID(sfOwner, A1); + sle->setFieldU32(sfSequence, 10); + + STArray credentials(sfAcceptedCredentials, 2); + for (std::size_t n = 0; n < 2; ++n) + { + auto cred = STObject::makeInnerObject(sfCredential); + cred.setAccountID(sfIssuer, A2); + auto credType = "cred_type" + std::to_string(n); + cred.setFieldVL( + sfCredentialType, Slice(credType.c_str(), credType.size())); + credentials.push_back(std::move(cred)); + } + sle->setFieldArray(sfAcceptedCredentials, credentials); + ac.view().insert(sle); + }; + void testPermissionedDomainInvariants() { @@ -1154,36 +1310,15 @@ class Invariants_test : public beast::unit_test::suite STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject& tx) {}}, {tecINVARIANT_FAILED, tecINVARIANT_FAILED}); - auto const createPD = [](ApplyContext& ac, - std::shared_ptr& sle, - Account const& A1, - Account const& A2) { - sle->setAccountID(sfOwner, A1); - sle->setFieldU32(sfSequence, 10); - - STArray credentials(sfAcceptedCredentials, 2); - for (std::size_t n = 0; n < 2; ++n) - { - auto cred = STObject::makeInnerObject(sfCredential); - cred.setAccountID(sfIssuer, A2); - auto credType = "cred_type" + std::to_string(n); - cred.setFieldVL( - sfCredentialType, Slice(credType.c_str(), credType.size())); - credentials.push_back(std::move(cred)); - } - sle->setFieldArray(sfAcceptedCredentials, credentials); - ac.view().insert(sle); - }; - testcase << "PermissionedDomain Set 1"; doInvariantCheck( {{"permissioned domain with no rules."}}, - [createPD](Account const& A1, Account const& A2, ApplyContext& ac) { + [&](Account const& A1, Account const& A2, ApplyContext& ac) { Keylet const pdKeylet = keylet::permissionedDomain(A1.id(), 10); auto slePd = std::make_shared(pdKeylet); // create PD - createPD(ac, slePd, A1, A2); + createPermissionedDomain(ac, slePd, A1, A2); // update PD with empty rules { @@ -1202,12 +1337,12 @@ class Invariants_test : public beast::unit_test::suite doInvariantCheck( {{"permissioned domain bad credentials size " + std::to_string(tooBig)}}, - [createPD](Account const& A1, Account const& A2, ApplyContext& ac) { + [&](Account const& A1, Account const& A2, ApplyContext& ac) { Keylet const pdKeylet = keylet::permissionedDomain(A1.id(), 10); auto slePd = std::make_shared(pdKeylet); // create PD - createPD(ac, slePd, A1, A2); + createPermissionedDomain(ac, slePd, A1, A2); // update PD { @@ -1237,12 +1372,12 @@ class Invariants_test : public beast::unit_test::suite testcase << "PermissionedDomain Set 3"; doInvariantCheck( {{"permissioned domain credentials aren't sorted"}}, - [createPD](Account const& A1, Account const& A2, ApplyContext& ac) { + [&](Account const& A1, Account const& A2, ApplyContext& ac) { Keylet const pdKeylet = keylet::permissionedDomain(A1.id(), 10); auto slePd = std::make_shared(pdKeylet); // create PD - createPD(ac, slePd, A1, A2); + createPermissionedDomain(ac, slePd, A1, A2); // update PD { @@ -1272,12 +1407,12 @@ class Invariants_test : public beast::unit_test::suite testcase << "PermissionedDomain Set 4"; doInvariantCheck( {{"permissioned domain credentials aren't unique"}}, - [createPD](Account const& A1, Account const& A2, ApplyContext& ac) { + [&](Account const& A1, Account const& A2, ApplyContext& ac) { Keylet const pdKeylet = keylet::permissionedDomain(A1.id(), 10); auto slePd = std::make_shared(pdKeylet); // create PD - createPD(ac, slePd, A1, A2); + createPermissionedDomain(ac, slePd, A1, A2); // update PD { @@ -1413,6 +1548,176 @@ class Invariants_test : public beast::unit_test::suite return true; }); } + + void + testPermissionedDEX() + { + using namespace test::jtx; + testcase << "PermissionedDEX"; + + doInvariantCheck( + {{"domain doesn't exist"}}, + [](Account const& A1, Account const&, ApplyContext& ac) { + Keylet const offerKey = keylet::offer(A1.id(), 10); + auto sleOffer = std::make_shared(offerKey); + sleOffer->setAccountID(sfAccount, A1); + sleOffer->setFieldAmount(sfTakerPays, A1["USD"](10)); + sleOffer->setFieldAmount(sfTakerGets, XRP(1)); + ac.view().insert(sleOffer); + return true; + }, + XRPAmount{}, + STTx{ + ttOFFER_CREATE, + [](STObject& tx) { + tx.setFieldH256( + sfDomainID, + uint256{ + "F10D0CC9A0F9A3CBF585B80BE09A186483668FDBDD39AA7E33" + "70F3649CE134E5"}); + Account const A1{"A1"}; + tx.setFieldAmount(sfTakerPays, A1["USD"](10)); + tx.setFieldAmount(sfTakerGets, XRP(1)); + }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}); + + // missing domain ID in offer object + doInvariantCheck( + {{"hybrid offer is malformed"}}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + Keylet const pdKeylet = keylet::permissionedDomain(A1.id(), 10); + auto slePd = std::make_shared(pdKeylet); + createPermissionedDomain(ac, slePd, A1, A2); + + Keylet const offerKey = keylet::offer(A2.id(), 10); + auto sleOffer = std::make_shared(offerKey); + sleOffer->setAccountID(sfAccount, A2); + sleOffer->setFieldAmount(sfTakerPays, A1["USD"](10)); + sleOffer->setFieldAmount(sfTakerGets, XRP(1)); + sleOffer->setFlag(lsfHybrid); + + STArray bookArr; + bookArr.push_back(STObject::makeInnerObject(sfBook)); + sleOffer->setFieldArray(sfAdditionalBooks, bookArr); + ac.view().insert(sleOffer); + return true; + }, + XRPAmount{}, + STTx{ttOFFER_CREATE, [&](STObject& tx) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}); + + // more than one entry in sfAdditionalBooks + doInvariantCheck( + {{"hybrid offer is malformed"}}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + Keylet const pdKeylet = keylet::permissionedDomain(A1.id(), 10); + auto slePd = std::make_shared(pdKeylet); + createPermissionedDomain(ac, slePd, A1, A2); + + Keylet const offerKey = keylet::offer(A2.id(), 10); + auto sleOffer = std::make_shared(offerKey); + sleOffer->setAccountID(sfAccount, A2); + sleOffer->setFieldAmount(sfTakerPays, A1["USD"](10)); + sleOffer->setFieldAmount(sfTakerGets, XRP(1)); + sleOffer->setFlag(lsfHybrid); + sleOffer->setFieldH256(sfDomainID, pdKeylet.key); + + STArray bookArr; + bookArr.push_back(STObject::makeInnerObject(sfBook)); + bookArr.push_back(STObject::makeInnerObject(sfBook)); + sleOffer->setFieldArray(sfAdditionalBooks, bookArr); + ac.view().insert(sleOffer); + return true; + }, + XRPAmount{}, + STTx{ttOFFER_CREATE, [&](STObject& tx) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}); + + // hybrid offer missing sfAdditionalBooks + doInvariantCheck( + {{"hybrid offer is malformed"}}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + Keylet const pdKeylet = keylet::permissionedDomain(A1.id(), 10); + auto slePd = std::make_shared(pdKeylet); + createPermissionedDomain(ac, slePd, A1, A2); + + Keylet const offerKey = keylet::offer(A2.id(), 10); + auto sleOffer = std::make_shared(offerKey); + sleOffer->setAccountID(sfAccount, A2); + sleOffer->setFieldAmount(sfTakerPays, A1["USD"](10)); + sleOffer->setFieldAmount(sfTakerGets, XRP(1)); + sleOffer->setFlag(lsfHybrid); + sleOffer->setFieldH256(sfDomainID, pdKeylet.key); + ac.view().insert(sleOffer); + return true; + }, + XRPAmount{}, + STTx{ttOFFER_CREATE, [&](STObject& tx) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}); + + doInvariantCheck( + {{"transaction consumed wrong domains"}}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + Keylet const pdKeylet = keylet::permissionedDomain(A1.id(), 10); + auto slePd = std::make_shared(pdKeylet); + createPermissionedDomain(ac, slePd, A1, A2); + + Keylet const badDomainKeylet = + keylet::permissionedDomain(A1.id(), 20); + auto sleBadPd = std::make_shared(badDomainKeylet); + createPermissionedDomain(ac, sleBadPd, A1, A2); + + Keylet const offerKey = keylet::offer(A2.id(), 10); + auto sleOffer = std::make_shared(offerKey); + sleOffer->setAccountID(sfAccount, A2); + sleOffer->setFieldAmount(sfTakerPays, A1["USD"](10)); + sleOffer->setFieldAmount(sfTakerGets, XRP(1)); + sleOffer->setFieldH256(sfDomainID, pdKeylet.key); + ac.view().insert(sleOffer); + return true; + }, + XRPAmount{}, + STTx{ + ttOFFER_CREATE, + [&](STObject& tx) { + Account const A1{"A1"}; + Keylet const badDomainKey = + keylet::permissionedDomain(A1.id(), 20); + tx.setFieldH256(sfDomainID, badDomainKey.key); + tx.setFieldAmount(sfTakerPays, A1["USD"](10)); + tx.setFieldAmount(sfTakerGets, XRP(1)); + }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}); + + doInvariantCheck( + {{"domain transaction affected regular offers"}}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + Keylet const pdKeylet = keylet::permissionedDomain(A1.id(), 10); + auto slePd = std::make_shared(pdKeylet); + createPermissionedDomain(ac, slePd, A1, A2); + + Keylet const offerKey = keylet::offer(A2.id(), 10); + auto sleOffer = std::make_shared(offerKey); + sleOffer->setAccountID(sfAccount, A2); + sleOffer->setFieldAmount(sfTakerPays, A1["USD"](10)); + sleOffer->setFieldAmount(sfTakerGets, XRP(1)); + ac.view().insert(sleOffer); + return true; + }, + XRPAmount{}, + STTx{ + ttOFFER_CREATE, + [&](STObject& tx) { + Account const A1{"A1"}; + Keylet const domainKey = + keylet::permissionedDomain(A1.id(), 10); + tx.setFieldH256(sfDomainID, domainKey.key); + tx.setFieldAmount(sfTakerPays, A1["USD"](10)); + tx.setFieldAmount(sfTakerGets, XRP(1)); + }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}); + } + public: void run() override @@ -1432,6 +1737,7 @@ public: testNFTokenPageInvariants(); testPermissionedDomainInvariants(); testValidPseudoAccounts(); + testPermissionedDEX(); } }; diff --git a/src/test/ledger/PaymentSandbox_test.cpp b/src/test/ledger/PaymentSandbox_test.cpp index 303e700f40..26b06a0034 100644 --- a/src/test/ledger/PaymentSandbox_test.cpp +++ b/src/test/ledger/PaymentSandbox_test.cpp @@ -420,8 +420,8 @@ public: testBalanceHook(features); }; using namespace jtx; - auto const sa = supported_amendments(); - testAll(sa - featureFlowCross); + auto const sa = testable_amendments(); + testAll(sa - featurePermissionedDEX); testAll(sa); } }; diff --git a/src/test/overlay/compression_test.cpp b/src/test/overlay/compression_test.cpp index 76c38fd59b..4ecbe7f232 100644 --- a/src/test/overlay/compression_test.cpp +++ b/src/test/overlay/compression_test.cpp @@ -473,17 +473,14 @@ public: Config c; std::stringstream str; str << "[reduce_relay]\n" - << "vp_enable=1\n" - << "vp_squelch=1\n" + << "vp_base_squelch_enable=1\n" << "[compression]\n" << enable << "\n"; c.loadFromString(str.str()); auto env = std::make_shared(*this); env->app().config().COMPRESSION = c.COMPRESSION; - env->app().config().VP_REDUCE_RELAY_ENABLE = - c.VP_REDUCE_RELAY_ENABLE; - env->app().config().VP_REDUCE_RELAY_SQUELCH = - c.VP_REDUCE_RELAY_SQUELCH; + env->app().config().VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE = + c.VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE; return env; }; auto handshake = [&](int outboundEnable, int inboundEnable) { @@ -496,7 +493,7 @@ public: env->app().config().COMPRESSION, false, env->app().config().TX_REDUCE_RELAY_ENABLE, - env->app().config().VP_REDUCE_RELAY_ENABLE); + env->app().config().VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE); http_request_type http_request; http_request.version(request.version()); http_request.base() = request.base(); diff --git a/src/test/overlay/reduce_relay_test.cpp b/src/test/overlay/reduce_relay_test.cpp index 18aebbe194..a8aafcfa06 100644 --- a/src/test/overlay/reduce_relay_test.cpp +++ b/src/test/overlay/reduce_relay_test.cpp @@ -17,6 +17,7 @@ */ //============================================================================== +#include #include #include @@ -32,6 +33,8 @@ #include +#include +#include #include #include @@ -517,7 +520,8 @@ class OverlaySim : public Overlay, public reduce_relay::SquelchHandler public: using id_t = Peer::id_t; using clock_type = ManualClock; - OverlaySim(Application& app) : slots_(app.logs(), *this), logs_(app.logs()) + OverlaySim(Application& app) + : slots_(app.logs(), *this, app.config()), logs_(app.logs()) { } @@ -986,7 +990,10 @@ protected: network_.overlay().isCountingState(validator); BEAST_EXPECT( countingState == false && - selected.size() == reduce_relay::MAX_SELECTED_PEERS); + selected.size() == + env_.app() + .config() + .VP_REDUCE_RELAY_SQUELCH_MAX_SELECTED_PEERS); } // Trigger Link Down or Peer Disconnect event @@ -1188,7 +1195,10 @@ protected: { BEAST_EXPECT( squelched == - MAX_PEERS - reduce_relay::MAX_SELECTED_PEERS); + MAX_PEERS - + env_.app() + .config() + .VP_REDUCE_RELAY_SQUELCH_MAX_SELECTED_PEERS); n++; } }, @@ -1197,7 +1207,9 @@ protected: purge, resetClock); auto selected = network_.overlay().getSelected(network_.validator(0)); - BEAST_EXPECT(selected.size() == reduce_relay::MAX_SELECTED_PEERS); + BEAST_EXPECT( + selected.size() == + env_.app().config().VP_REDUCE_RELAY_SQUELCH_MAX_SELECTED_PEERS); BEAST_EXPECT(n == 1); // only one selection round auto res = checkCounting(network_.validator(0), false); BEAST_EXPECT(res); @@ -1261,7 +1273,11 @@ protected: unsquelched++; }); BEAST_EXPECT( - unsquelched == MAX_PEERS - reduce_relay::MAX_SELECTED_PEERS); + unsquelched == + MAX_PEERS - + env_.app() + .config() + .VP_REDUCE_RELAY_SQUELCH_MAX_SELECTED_PEERS); BEAST_EXPECT(checkCounting(network_.validator(0), true)); }); } @@ -1282,7 +1298,11 @@ protected: }); auto peers = network_.overlay().getPeers(network_.validator(0)); BEAST_EXPECT( - unsquelched == MAX_PEERS - reduce_relay::MAX_SELECTED_PEERS); + unsquelched == + MAX_PEERS - + env_.app() + .config() + .VP_REDUCE_RELAY_SQUELCH_MAX_SELECTED_PEERS); BEAST_EXPECT(checkCounting(network_.validator(0), true)); }); } @@ -1314,42 +1334,164 @@ protected: void testConfig(bool log) { - doTest("Config Test", log, [&](bool log) { + doTest("Test Config - squelch enabled (legacy)", log, [&](bool log) { Config c; std::string toLoad(R"rippleConfig( [reduce_relay] vp_enable=1 -vp_squelch=1 )rippleConfig"); c.loadFromString(toLoad); - BEAST_EXPECT(c.VP_REDUCE_RELAY_ENABLE == true); - BEAST_EXPECT(c.VP_REDUCE_RELAY_SQUELCH == true); + BEAST_EXPECT(c.VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE == true); + }); + + doTest("Test Config - squelch disabled (legacy)", log, [&](bool log) { + Config c; + + std::string toLoad(R"rippleConfig( +[reduce_relay] +vp_enable=0 +)rippleConfig"); + + c.loadFromString(toLoad); + BEAST_EXPECT(c.VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE == false); Config c1; - toLoad = (R"rippleConfig( + toLoad = R"rippleConfig( [reduce_relay] -vp_enable=0 -vp_squelch=0 -)rippleConfig"); +)rippleConfig"; c1.loadFromString(toLoad); - BEAST_EXPECT(c1.VP_REDUCE_RELAY_ENABLE == false); - BEAST_EXPECT(c1.VP_REDUCE_RELAY_SQUELCH == false); + BEAST_EXPECT(c1.VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE == false); + }); + + doTest("Test Config - squelch enabled", log, [&](bool log) { + Config c; + + std::string toLoad(R"rippleConfig( +[reduce_relay] +vp_base_squelch_enable=1 +)rippleConfig"); + + c.loadFromString(toLoad); + BEAST_EXPECT(c.VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE == true); + }); + + doTest("Test Config - squelch disabled", log, [&](bool log) { + Config c; + + std::string toLoad(R"rippleConfig( +[reduce_relay] +vp_base_squelch_enable=0 +)rippleConfig"); + + c.loadFromString(toLoad); + BEAST_EXPECT(c.VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE == false); + }); + + doTest("Test Config - legacy and new", log, [&](bool log) { + Config c; + + std::string toLoad(R"rippleConfig( +[reduce_relay] +vp_base_squelch_enable=0 +vp_enable=0 +)rippleConfig"); + + std::string error; + auto const expectedError = + "Invalid reduce_relay" + " cannot specify both vp_base_squelch_enable and vp_enable " + "options. " + "vp_enable was deprecated and replaced by " + "vp_base_squelch_enable"; + + try + { + c.loadFromString(toLoad); + } + catch (std::runtime_error& e) + { + error = e.what(); + } + + BEAST_EXPECT(error == expectedError); + }); + + doTest("Test Config - max selected peers", log, [&](bool log) { + Config c; + + std::string toLoad(R"rippleConfig( +[reduce_relay] +)rippleConfig"); + + c.loadFromString(toLoad); + BEAST_EXPECT(c.VP_REDUCE_RELAY_SQUELCH_MAX_SELECTED_PEERS == 5); + + Config c1; + + toLoad = R"rippleConfig( +[reduce_relay] +vp_base_squelch_max_selected_peers=6 +)rippleConfig"; + + c1.loadFromString(toLoad); + BEAST_EXPECT(c1.VP_REDUCE_RELAY_SQUELCH_MAX_SELECTED_PEERS == 6); Config c2; toLoad = R"rippleConfig( [reduce_relay] -vp_enabled=1 -vp_squelched=1 +vp_base_squelch_max_selected_peers=2 )rippleConfig"; - c2.loadFromString(toLoad); - BEAST_EXPECT(c2.VP_REDUCE_RELAY_ENABLE == false); - BEAST_EXPECT(c2.VP_REDUCE_RELAY_SQUELCH == false); + std::string error; + auto const expectedError = + "Invalid reduce_relay" + " vp_base_squelch_max_selected_peers must be " + "greater than or equal to 3"; + try + { + c2.loadFromString(toLoad); + } + catch (std::runtime_error& e) + { + error = e.what(); + } + + BEAST_EXPECT(error == expectedError); + }); + } + + void + testBaseSquelchReady(bool log) + { + doTest("BaseSquelchReady", log, [&](bool log) { + ManualClock::reset(); + auto createSlots = [&](bool baseSquelchEnabled) + -> reduce_relay::Slots { + env_.app().config().VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE = + baseSquelchEnabled; + return reduce_relay::Slots( + env_.app().logs(), network_.overlay(), env_.app().config()); + }; + // base squelching must not be ready if squelching is disabled + BEAST_EXPECT(!createSlots(false).baseSquelchReady()); + + // base squelch must not be ready as not enough time passed from + // bootup + BEAST_EXPECT(!createSlots(true).baseSquelchReady()); + + ManualClock::advance(reduce_relay::WAIT_ON_BOOTUP + minutes{1}); + + // base squelch enabled and bootup time passed + BEAST_EXPECT(createSlots(true).baseSquelchReady()); + + // even if time passed, base squelching must not be ready if turned + // off in the config + BEAST_EXPECT(!createSlots(false).baseSquelchReady()); }); } @@ -1425,7 +1567,7 @@ vp_squelched=1 auto run = [&](int npeers) { handler.maxDuration_ = 0; reduce_relay::Slots slots( - env_.app().logs(), handler); + env_.app().logs(), handler, env_.app().config()); // 1st message from a new peer switches the slot // to counting state and resets the counts of all peers + // MAX_MESSAGE_THRESHOLD + 1 messages to reach the threshold @@ -1503,14 +1645,12 @@ vp_squelched=1 std::stringstream str; str << "[reduce_relay]\n" << "vp_enable=" << enable << "\n" - << "vp_squelch=" << enable << "\n" << "[compression]\n" << "1\n"; c.loadFromString(str.str()); - env_.app().config().VP_REDUCE_RELAY_ENABLE = - c.VP_REDUCE_RELAY_ENABLE; - env_.app().config().VP_REDUCE_RELAY_SQUELCH = - c.VP_REDUCE_RELAY_SQUELCH; + env_.app().config().VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE = + c.VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE; + env_.app().config().COMPRESSION = c.COMPRESSION; }; auto handshake = [&](int outboundEnable, int inboundEnable) { @@ -1523,7 +1663,7 @@ vp_squelched=1 env_.app().config().COMPRESSION, false, env_.app().config().TX_REDUCE_RELAY_ENABLE, - env_.app().config().VP_REDUCE_RELAY_ENABLE); + env_.app().config().VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE); http_request_type http_request; http_request.version(request.version()); http_request.base() = request.base(); @@ -1563,7 +1703,13 @@ vp_squelched=1 Network network_; public: - reduce_relay_test() : env_(*this), network_(env_.app()) + reduce_relay_test() + : env_(*this, jtx::envconfig([](std::unique_ptr cfg) { + cfg->VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE = true; + cfg->VP_REDUCE_RELAY_SQUELCH_MAX_SELECTED_PEERS = 6; + return cfg; + })) + , network_(env_.app()) { } @@ -1582,6 +1728,7 @@ public: testInternalHashRouter(log); testRandomSquelch(log); testHandshake(log); + testBaseSquelchReady(log); } }; diff --git a/src/test/protocol/Issue_test.cpp b/src/test/protocol/Issue_test.cpp index 53ebf5be24..35f3a3bd8c 100644 --- a/src/test/protocol/Issue_test.cpp +++ b/src/test/protocol/Issue_test.cpp @@ -22,7 +22,10 @@ #include #include +#include + #include +#include #include #include #include @@ -46,6 +49,8 @@ namespace ripple { class Issue_test : public beast::unit_test::suite { public: + using Domain = uint256; + // Comparison, hash tests for uint60 (via base_uint) template void @@ -239,6 +244,120 @@ public: } } + template + void + testIssueDomainSet() + { + Currency const c1(1); + AccountID const i1(1); + Currency const c2(2); + AccountID const i2(2); + Issue const a1(c1, i1); + Issue const a2(c2, i2); + uint256 const domain1{1}; + uint256 const domain2{2}; + + Set c; + + c.insert(std::make_pair(a1, domain1)); + if (!BEAST_EXPECT(c.size() == 1)) + return; + c.insert(std::make_pair(a2, domain1)); + if (!BEAST_EXPECT(c.size() == 2)) + return; + c.insert(std::make_pair(a2, domain2)); + if (!BEAST_EXPECT(c.size() == 3)) + return; + + if (!BEAST_EXPECT(c.erase(std::make_pair(Issue(c1, i2), domain1)) == 0)) + return; + if (!BEAST_EXPECT(c.erase(std::make_pair(a1, domain1)) == 1)) + return; + if (!BEAST_EXPECT(c.erase(std::make_pair(a2, domain1)) == 1)) + return; + if (!BEAST_EXPECT(c.erase(std::make_pair(a2, domain2)) == 1)) + return; + if (!BEAST_EXPECT(c.empty())) + return; + } + + template + void + testIssueDomainMap() + { + Currency const c1(1); + AccountID const i1(1); + Currency const c2(2); + AccountID const i2(2); + Issue const a1(c1, i1); + Issue const a2(c2, i2); + uint256 const domain1{1}; + uint256 const domain2{2}; + + Map c; + + c.insert(std::make_pair(std::make_pair(a1, domain1), 1)); + if (!BEAST_EXPECT(c.size() == 1)) + return; + c.insert(std::make_pair(std::make_pair(a2, domain1), 2)); + if (!BEAST_EXPECT(c.size() == 2)) + return; + c.insert(std::make_pair(std::make_pair(a2, domain2), 2)); + if (!BEAST_EXPECT(c.size() == 3)) + return; + + if (!BEAST_EXPECT(c.erase(std::make_pair(Issue(c1, i2), domain1)) == 0)) + return; + if (!BEAST_EXPECT(c.erase(std::make_pair(a1, domain1)) == 1)) + return; + if (!BEAST_EXPECT(c.erase(std::make_pair(a2, domain1)) == 1)) + return; + if (!BEAST_EXPECT(c.erase(std::make_pair(a2, domain2)) == 1)) + return; + if (!BEAST_EXPECT(c.empty())) + return; + } + + void + testIssueDomainSets() + { + testcase("std::set >"); + testIssueDomainSet>>(); + + testcase("std::set >"); + testIssueDomainSet>>(); + + testcase("hash_set >"); + testIssueDomainSet>>(); + + testcase("hash_set >"); + testIssueDomainSet>>(); + } + + void + testIssueDomainMaps() + { + testcase("std::map , int>"); + testIssueDomainMap, int>>(); + + testcase("std::map , int>"); + testIssueDomainMap, int>>(); + +#if RIPPLE_ASSETS_ENABLE_STD_HASH + testcase("hash_map , int>"); + testIssueDomainMap, int>>(); + + testcase("hash_map , int>"); + testIssueDomainMap, int>>(); + + testcase("hardened_hash_map , int>"); + testIssueDomainMap, int>>(); + + testcase("hardened_hash_map , int>"); + testIssueDomainMap, int>>(); +#endif + } + void testIssueSets() { @@ -306,15 +425,88 @@ public: Issue a2(c1, i2); Issue a3(c2, i2); Issue a4(c3, i2); + uint256 const domain1{1}; + uint256 const domain2{2}; - BEAST_EXPECT(Book(a1, a2) != Book(a2, a3)); - BEAST_EXPECT(Book(a1, a2) < Book(a2, a3)); - BEAST_EXPECT(Book(a1, a2) <= Book(a2, a3)); - BEAST_EXPECT(Book(a2, a3) <= Book(a2, a3)); - BEAST_EXPECT(Book(a2, a3) == Book(a2, a3)); - BEAST_EXPECT(Book(a2, a3) >= Book(a2, a3)); - BEAST_EXPECT(Book(a3, a4) >= Book(a2, a3)); - BEAST_EXPECT(Book(a3, a4) > Book(a2, a3)); + // Books without domains + BEAST_EXPECT(Book(a1, a2, std::nullopt) != Book(a2, a3, std::nullopt)); + BEAST_EXPECT(Book(a1, a2, std::nullopt) < Book(a2, a3, std::nullopt)); + BEAST_EXPECT(Book(a1, a2, std::nullopt) <= Book(a2, a3, std::nullopt)); + BEAST_EXPECT(Book(a2, a3, std::nullopt) <= Book(a2, a3, std::nullopt)); + BEAST_EXPECT(Book(a2, a3, std::nullopt) == Book(a2, a3, std::nullopt)); + BEAST_EXPECT(Book(a2, a3, std::nullopt) >= Book(a2, a3, std::nullopt)); + BEAST_EXPECT(Book(a3, a4, std::nullopt) >= Book(a2, a3, std::nullopt)); + BEAST_EXPECT(Book(a3, a4, std::nullopt) > Book(a2, a3, std::nullopt)); + + // test domain books + { + // Books with different domains + BEAST_EXPECT(Book(a2, a3, domain1) != Book(a2, a3, domain2)); + BEAST_EXPECT(Book(a2, a3, domain1) < Book(a2, a3, domain2)); + BEAST_EXPECT(Book(a2, a3, domain2) > Book(a2, a3, domain1)); + + // One Book has a domain, the other does not + BEAST_EXPECT(Book(a2, a3, domain1) != Book(a2, a3, std::nullopt)); + BEAST_EXPECT(Book(a2, a3, std::nullopt) < Book(a2, a3, domain1)); + BEAST_EXPECT(Book(a2, a3, domain1) > Book(a2, a3, std::nullopt)); + + // Both Books have the same domain + BEAST_EXPECT(Book(a2, a3, domain1) == Book(a2, a3, domain1)); + BEAST_EXPECT(Book(a2, a3, domain2) == Book(a2, a3, domain2)); + BEAST_EXPECT( + Book(a2, a3, std::nullopt) == Book(a2, a3, std::nullopt)); + + // Both Books have no domain + BEAST_EXPECT( + Book(a2, a3, std::nullopt) == Book(a2, a3, std::nullopt)); + + // Testing comparisons with >= and <= + + // When comparing books with domain1 vs domain2 + BEAST_EXPECT(Book(a2, a3, domain1) <= Book(a2, a3, domain2)); + BEAST_EXPECT(Book(a2, a3, domain2) >= Book(a2, a3, domain1)); + BEAST_EXPECT(Book(a2, a3, domain1) >= Book(a2, a3, domain1)); + BEAST_EXPECT(Book(a2, a3, domain2) <= Book(a2, a3, domain2)); + + // One Book has domain1 and the other has no domain + BEAST_EXPECT(Book(a2, a3, domain1) > Book(a2, a3, std::nullopt)); + BEAST_EXPECT(Book(a2, a3, std::nullopt) < Book(a2, a3, domain1)); + + // One Book has domain2 and the other has no domain + BEAST_EXPECT(Book(a2, a3, domain2) > Book(a2, a3, std::nullopt)); + BEAST_EXPECT(Book(a2, a3, std::nullopt) < Book(a2, a3, domain2)); + + // Comparing two Books with no domains + BEAST_EXPECT( + Book(a2, a3, std::nullopt) <= Book(a2, a3, std::nullopt)); + BEAST_EXPECT( + Book(a2, a3, std::nullopt) >= Book(a2, a3, std::nullopt)); + + // Test case where domain1 is less than domain2 + BEAST_EXPECT(Book(a2, a3, domain1) <= Book(a2, a3, domain2)); + BEAST_EXPECT(Book(a2, a3, domain2) >= Book(a2, a3, domain1)); + + // Test case where domain2 is equal to domain1 + BEAST_EXPECT(Book(a2, a3, domain1) >= Book(a2, a3, domain1)); + BEAST_EXPECT(Book(a2, a3, domain1) <= Book(a2, a3, domain1)); + + // More test cases involving a4 (with domain2) + + // Comparing Book with domain2 (a4) to a Book with domain1 + BEAST_EXPECT(Book(a2, a3, domain1) < Book(a3, a4, domain2)); + BEAST_EXPECT(Book(a3, a4, domain2) > Book(a2, a3, domain1)); + + // Comparing Book with domain2 (a4) to a Book with no domain + BEAST_EXPECT(Book(a3, a4, domain2) > Book(a2, a3, std::nullopt)); + BEAST_EXPECT(Book(a2, a3, std::nullopt) < Book(a3, a4, domain2)); + + // Comparing Book with domain2 (a4) to a Book with the same domain + BEAST_EXPECT(Book(a3, a4, domain2) == Book(a3, a4, domain2)); + + // Comparing Book with domain2 (a4) to a Book with domain1 + BEAST_EXPECT(Book(a2, a3, domain1) < Book(a3, a4, domain2)); + BEAST_EXPECT(Book(a3, a4, domain2) > Book(a2, a3, domain1)); + } std::hash hash; @@ -336,18 +528,99 @@ public: // log << std::hex << hash (Book (a3, a4)); // log << std::hex << hash (Book (a3, a4)); - BEAST_EXPECT(hash(Book(a1, a2)) == hash(Book(a1, a2))); - BEAST_EXPECT(hash(Book(a1, a3)) == hash(Book(a1, a3))); - BEAST_EXPECT(hash(Book(a1, a4)) == hash(Book(a1, a4))); - BEAST_EXPECT(hash(Book(a2, a3)) == hash(Book(a2, a3))); - BEAST_EXPECT(hash(Book(a2, a4)) == hash(Book(a2, a4))); - BEAST_EXPECT(hash(Book(a3, a4)) == hash(Book(a3, a4))); + BEAST_EXPECT( + hash(Book(a1, a2, std::nullopt)) == + hash(Book(a1, a2, std::nullopt))); + BEAST_EXPECT( + hash(Book(a1, a3, std::nullopt)) == + hash(Book(a1, a3, std::nullopt))); + BEAST_EXPECT( + hash(Book(a1, a4, std::nullopt)) == + hash(Book(a1, a4, std::nullopt))); + BEAST_EXPECT( + hash(Book(a2, a3, std::nullopt)) == + hash(Book(a2, a3, std::nullopt))); + BEAST_EXPECT( + hash(Book(a2, a4, std::nullopt)) == + hash(Book(a2, a4, std::nullopt))); + BEAST_EXPECT( + hash(Book(a3, a4, std::nullopt)) == + hash(Book(a3, a4, std::nullopt))); - BEAST_EXPECT(hash(Book(a1, a2)) != hash(Book(a1, a3))); - BEAST_EXPECT(hash(Book(a1, a2)) != hash(Book(a1, a4))); - BEAST_EXPECT(hash(Book(a1, a2)) != hash(Book(a2, a3))); - BEAST_EXPECT(hash(Book(a1, a2)) != hash(Book(a2, a4))); - BEAST_EXPECT(hash(Book(a1, a2)) != hash(Book(a3, a4))); + BEAST_EXPECT( + hash(Book(a1, a2, std::nullopt)) != + hash(Book(a1, a3, std::nullopt))); + BEAST_EXPECT( + hash(Book(a1, a2, std::nullopt)) != + hash(Book(a1, a4, std::nullopt))); + BEAST_EXPECT( + hash(Book(a1, a2, std::nullopt)) != + hash(Book(a2, a3, std::nullopt))); + BEAST_EXPECT( + hash(Book(a1, a2, std::nullopt)) != + hash(Book(a2, a4, std::nullopt))); + BEAST_EXPECT( + hash(Book(a1, a2, std::nullopt)) != + hash(Book(a3, a4, std::nullopt))); + + // Books with domain + BEAST_EXPECT( + hash(Book(a1, a2, domain1)) == hash(Book(a1, a2, domain1))); + BEAST_EXPECT( + hash(Book(a1, a3, domain1)) == hash(Book(a1, a3, domain1))); + BEAST_EXPECT( + hash(Book(a1, a4, domain1)) == hash(Book(a1, a4, domain1))); + BEAST_EXPECT( + hash(Book(a2, a3, domain1)) == hash(Book(a2, a3, domain1))); + BEAST_EXPECT( + hash(Book(a2, a4, domain1)) == hash(Book(a2, a4, domain1))); + BEAST_EXPECT( + hash(Book(a3, a4, domain1)) == hash(Book(a3, a4, domain1))); + BEAST_EXPECT( + hash(Book(a1, a2, std::nullopt)) == + hash(Book(a1, a2, std::nullopt))); + + // Comparing Books with domain1 vs no domain + BEAST_EXPECT( + hash(Book(a1, a2, std::nullopt)) != hash(Book(a1, a2, domain1))); + BEAST_EXPECT( + hash(Book(a1, a3, std::nullopt)) != hash(Book(a1, a3, domain1))); + BEAST_EXPECT( + hash(Book(a1, a4, std::nullopt)) != hash(Book(a1, a4, domain1))); + BEAST_EXPECT( + hash(Book(a2, a3, std::nullopt)) != hash(Book(a2, a3, domain1))); + BEAST_EXPECT( + hash(Book(a2, a4, std::nullopt)) != hash(Book(a2, a4, domain1))); + BEAST_EXPECT( + hash(Book(a3, a4, std::nullopt)) != hash(Book(a3, a4, domain1))); + + // Books with domain1 but different Issues + BEAST_EXPECT( + hash(Book(a1, a2, domain1)) != hash(Book(a1, a3, domain1))); + BEAST_EXPECT( + hash(Book(a1, a2, domain1)) != hash(Book(a1, a4, domain1))); + BEAST_EXPECT( + hash(Book(a2, a3, domain1)) != hash(Book(a2, a4, domain1))); + BEAST_EXPECT( + hash(Book(a1, a2, domain1)) != hash(Book(a2, a3, domain1))); + BEAST_EXPECT( + hash(Book(a2, a4, domain1)) != hash(Book(a3, a4, domain1))); + BEAST_EXPECT( + hash(Book(a3, a4, domain1)) != hash(Book(a1, a4, domain1))); + + // Books with domain1 and domain2 + BEAST_EXPECT( + hash(Book(a1, a2, domain1)) != hash(Book(a1, a2, domain2))); + BEAST_EXPECT( + hash(Book(a1, a3, domain1)) != hash(Book(a1, a3, domain2))); + BEAST_EXPECT( + hash(Book(a1, a4, domain1)) != hash(Book(a1, a4, domain2))); + BEAST_EXPECT( + hash(Book(a2, a3, domain1)) != hash(Book(a2, a3, domain2))); + BEAST_EXPECT( + hash(Book(a2, a4, domain1)) != hash(Book(a2, a4, domain2))); + BEAST_EXPECT( + hash(Book(a3, a4, domain1)) != hash(Book(a3, a4, domain2))); } //-------------------------------------------------------------------------- @@ -362,8 +635,16 @@ public: AccountID const i2(2); Issue const a1(c1, i1); Issue const a2(c2, i2); - Book const b1(a1, a2); - Book const b2(a2, a1); + Book const b1(a1, a2, std::nullopt); + Book const b2(a2, a1, std::nullopt); + + uint256 const domain1{1}; + uint256 const domain2{2}; + + Book const b1_d1(a1, a2, domain1); + Book const b2_d1(a2, a1, domain1); + Book const b1_d2(a1, a2, domain2); + Book const b2_d2(a2, a1, domain2); { Set c; @@ -375,11 +656,11 @@ public: if (!BEAST_EXPECT(c.size() == 2)) return; - if (!BEAST_EXPECT(c.erase(Book(a1, a1)) == 0)) + if (!BEAST_EXPECT(c.erase(Book(a1, a1, std::nullopt)) == 0)) return; - if (!BEAST_EXPECT(c.erase(Book(a1, a2)) == 1)) + if (!BEAST_EXPECT(c.erase(Book(a1, a2, std::nullopt)) == 1)) return; - if (!BEAST_EXPECT(c.erase(Book(a2, a1)) == 1)) + if (!BEAST_EXPECT(c.erase(Book(a2, a1, std::nullopt)) == 1)) return; if (!BEAST_EXPECT(c.empty())) return; @@ -395,11 +676,11 @@ public: if (!BEAST_EXPECT(c.size() == 2)) return; - if (!BEAST_EXPECT(c.erase(Book(a1, a1)) == 0)) + if (!BEAST_EXPECT(c.erase(Book(a1, a1, std::nullopt)) == 0)) return; - if (!BEAST_EXPECT(c.erase(Book(a1, a2)) == 1)) + if (!BEAST_EXPECT(c.erase(Book(a1, a2, std::nullopt)) == 1)) return; - if (!BEAST_EXPECT(c.erase(Book(a2, a1)) == 1)) + if (!BEAST_EXPECT(c.erase(Book(a2, a1, std::nullopt)) == 1)) return; if (!BEAST_EXPECT(c.empty())) return; @@ -413,6 +694,66 @@ public: return; #endif } + + { + Set c; + + c.insert(b1_d1); + if (!BEAST_EXPECT(c.size() == 1)) + return; + c.insert(b2_d1); + if (!BEAST_EXPECT(c.size() == 2)) + return; + c.insert(b1_d2); + if (!BEAST_EXPECT(c.size() == 3)) + return; + c.insert(b2_d2); + if (!BEAST_EXPECT(c.size() == 4)) + return; + + // Try removing non-existent elements + if (!BEAST_EXPECT(c.erase(Book(a2, a2, domain1)) == 0)) + return; + + if (!BEAST_EXPECT(c.erase(Book(a1, a2, domain1)) == 1)) + return; + if (!BEAST_EXPECT(c.erase(Book(a2, a1, domain1)) == 1)) + return; + if (!BEAST_EXPECT(c.size() == 2)) + return; + + if (!BEAST_EXPECT(c.erase(Book(a1, a2, domain2)) == 1)) + return; + if (!BEAST_EXPECT(c.erase(Book(a2, a1, domain2)) == 1)) + return; + if (!BEAST_EXPECT(c.empty())) + return; + } + + { + Set c; + + c.insert(b1); + c.insert(b2); + c.insert(b1_d1); + c.insert(b2_d1); + if (!BEAST_EXPECT(c.size() == 4)) + return; + + if (!BEAST_EXPECT(c.erase(Book(a1, a2, std::nullopt)) == 1)) + return; + if (!BEAST_EXPECT(c.erase(Book(a2, a1, std::nullopt)) == 1)) + return; + if (!BEAST_EXPECT(c.size() == 2)) + return; + + if (!BEAST_EXPECT(c.erase(Book(a1, a2, domain1)) == 1)) + return; + if (!BEAST_EXPECT(c.erase(Book(a2, a1, domain1)) == 1)) + return; + if (!BEAST_EXPECT(c.empty())) + return; + } } template @@ -425,8 +766,16 @@ public: AccountID const i2(2); Issue const a1(c1, i1); Issue const a2(c2, i2); - Book const b1(a1, a2); - Book const b2(a2, a1); + Book const b1(a1, a2, std::nullopt); + Book const b2(a2, a1, std::nullopt); + + uint256 const domain1{1}; + uint256 const domain2{2}; + + Book const b1_d1(a1, a2, domain1); + Book const b2_d1(a2, a1, domain1); + Book const b1_d2(a1, a2, domain2); + Book const b2_d2(a2, a1, domain2); // typename Map::value_type value_type; // std::pair value_type; @@ -443,11 +792,11 @@ public: if (!BEAST_EXPECT(c.size() == 2)) return; - if (!BEAST_EXPECT(c.erase(Book(a1, a1)) == 0)) + if (!BEAST_EXPECT(c.erase(Book(a1, a1, std::nullopt)) == 0)) return; - if (!BEAST_EXPECT(c.erase(Book(a1, a2)) == 1)) + if (!BEAST_EXPECT(c.erase(Book(a1, a2, std::nullopt)) == 1)) return; - if (!BEAST_EXPECT(c.erase(Book(a2, a1)) == 1)) + if (!BEAST_EXPECT(c.erase(Book(a2, a1, std::nullopt)) == 1)) return; if (!BEAST_EXPECT(c.empty())) return; @@ -465,11 +814,77 @@ public: if (!BEAST_EXPECT(c.size() == 2)) return; - if (!BEAST_EXPECT(c.erase(Book(a1, a1)) == 0)) + if (!BEAST_EXPECT(c.erase(Book(a1, a1, std::nullopt)) == 0)) return; - if (!BEAST_EXPECT(c.erase(Book(a1, a2)) == 1)) + if (!BEAST_EXPECT(c.erase(Book(a1, a2, std::nullopt)) == 1)) return; - if (!BEAST_EXPECT(c.erase(Book(a2, a1)) == 1)) + if (!BEAST_EXPECT(c.erase(Book(a2, a1, std::nullopt)) == 1)) + return; + if (!BEAST_EXPECT(c.empty())) + return; + } + + { + Map c; + + c.insert(std::make_pair(b1_d1, 10)); + if (!BEAST_EXPECT(c.size() == 1)) + return; + c.insert(std::make_pair(b2_d1, 20)); + if (!BEAST_EXPECT(c.size() == 2)) + return; + c.insert(std::make_pair(b1_d2, 30)); + if (!BEAST_EXPECT(c.size() == 3)) + return; + c.insert(std::make_pair(b2_d2, 40)); + if (!BEAST_EXPECT(c.size() == 4)) + return; + + // Try removing non-existent elements + if (!BEAST_EXPECT(c.erase(Book(a2, a2, domain1)) == 0)) + return; + + if (!BEAST_EXPECT(c.erase(Book(a1, a2, domain1)) == 1)) + return; + if (!BEAST_EXPECT(c.erase(Book(a2, a1, domain1)) == 1)) + return; + if (!BEAST_EXPECT(c.size() == 2)) + return; + + if (!BEAST_EXPECT(c.erase(Book(a1, a2, domain2)) == 1)) + return; + if (!BEAST_EXPECT(c.erase(Book(a2, a1, domain2)) == 1)) + return; + if (!BEAST_EXPECT(c.empty())) + return; + } + + { + Map c; + + c.insert(std::make_pair(b1, 1)); + c.insert(std::make_pair(b2, 2)); + c.insert(std::make_pair(b1_d1, 3)); + c.insert(std::make_pair(b2_d1, 4)); + if (!BEAST_EXPECT(c.size() == 4)) + return; + + // Try removing non-existent elements + if (!BEAST_EXPECT(c.erase(Book(a1, a1, domain1)) == 0)) + return; + if (!BEAST_EXPECT(c.erase(Book(a2, a2, domain2)) == 0)) + return; + + if (!BEAST_EXPECT(c.erase(Book(a1, a2, std::nullopt)) == 1)) + return; + if (!BEAST_EXPECT(c.erase(Book(a2, a1, std::nullopt)) == 1)) + return; + if (!BEAST_EXPECT(c.size() == 2)) + return; + + if (!BEAST_EXPECT(c.erase(Book(a1, a2, domain1)) == 1)) + return; + if (!BEAST_EXPECT(c.erase(Book(a2, a1, domain1)) == 1)) return; if (!BEAST_EXPECT(c.empty())) return; @@ -556,6 +971,10 @@ public: testBookSets(); testBookMaps(); + + // --- + testIssueDomainSets(); + testIssueDomainMaps(); } }; diff --git a/src/test/protocol/STAmount_test.cpp b/src/test/protocol/STAmount_test.cpp index 712c91000e..d62241f2f4 100644 --- a/src/test/protocol/STAmount_test.cpp +++ b/src/test/protocol/STAmount_test.cpp @@ -17,6 +17,8 @@ */ //============================================================================== +#include + #include #include #include @@ -668,6 +670,366 @@ public: } } + void + testCanAddXRP() + { + testcase("can add xrp"); + + // Adding zero + { + STAmount amt1(XRPAmount(0)); + STAmount amt2(XRPAmount(1000)); + BEAST_EXPECT(canAdd(amt1, amt2) == true); + } + + // Adding zero + { + STAmount amt1(XRPAmount(1000)); + STAmount amt2(XRPAmount(0)); + BEAST_EXPECT(canAdd(amt1, amt2) == true); + } + + // Adding two positive XRP amounts + { + STAmount amt1(XRPAmount(500)); + STAmount amt2(XRPAmount(1500)); + BEAST_EXPECT(canAdd(amt1, amt2) == true); + } + + // Adding two negative XRP amounts + { + STAmount amt1(XRPAmount(-500)); + STAmount amt2(XRPAmount(-1500)); + BEAST_EXPECT(canAdd(amt1, amt2) == true); + } + + // Adding a positive and a negative XRP amount + { + STAmount amt1(XRPAmount(1000)); + STAmount amt2(XRPAmount(-1000)); + BEAST_EXPECT(canAdd(amt1, amt2) == true); + } + + // Overflow check for max XRP amounts + { + STAmount amt1(std::numeric_limits::max()); + STAmount amt2(XRPAmount(1)); + BEAST_EXPECT(canAdd(amt1, amt2) == false); + } + + // Overflow check for min XRP amounts + { + STAmount amt1(std::numeric_limits::max()); + amt1 += XRPAmount(1); + STAmount amt2(XRPAmount(-1)); + BEAST_EXPECT(canAdd(amt1, amt2) == false); + } + } + + void + testCanAddIOU() + { + testcase("can add iou"); + + Issue const usd{Currency(0x5553440000000000), AccountID(0x4985601)}; + Issue const eur{Currency(0x4555520000000000), AccountID(0x4985601)}; + + // Adding two IOU amounts + { + STAmount amt1(usd, 500); + STAmount amt2(usd, 1500); + BEAST_EXPECT(canAdd(amt1, amt2) == true); + } + + // Adding a positive and a negative IOU amount + { + STAmount amt1(usd, 1000); + STAmount amt2(usd, -1000); + BEAST_EXPECT(canAdd(amt1, amt2) == true); + } + + // Overflow check for max IOU amounts + { + STAmount amt1(usd, std::numeric_limits::max()); + STAmount amt2(usd, 1); + BEAST_EXPECT(canAdd(amt1, amt2) == false); + } + + // Overflow check for min IOU amounts + { + STAmount amt1(usd, std::numeric_limits::min()); + STAmount amt2(usd, -1); + BEAST_EXPECT(canAdd(amt1, amt2) == false); + } + + // Adding XRP and IOU + { + STAmount amt1(XRPAmount(1)); + STAmount amt2(usd, 1); + BEAST_EXPECT(canAdd(amt1, amt2) == false); + } + + // Adding different IOU issues (non zero) + { + STAmount amt1(usd, 1000); + STAmount amt2(eur, 500); + BEAST_EXPECT(canAdd(amt1, amt2) == false); + } + + // Adding different IOU issues (zero) + { + STAmount amt1(usd, 0); + STAmount amt2(eur, 500); + BEAST_EXPECT(canAdd(amt1, amt2) == false); + } + } + + void + testCanAddMPT() + { + testcase("can add mpt"); + + MPTIssue const mpt{MPTIssue{makeMptID(1, AccountID(0x4985601))}}; + MPTIssue const mpt2{MPTIssue{makeMptID(2, AccountID(0x4985601))}}; + + // Adding zero + { + STAmount amt1(mpt, 0); + STAmount amt2(mpt, 1000); + BEAST_EXPECT(canAdd(amt1, amt2) == true); + } + + // Adding zero + { + STAmount amt1(mpt, 1000); + STAmount amt2(mpt, 0); + BEAST_EXPECT(canAdd(amt1, amt2) == true); + } + + // Adding two positive MPT amounts + { + STAmount amt1(mpt, 500); + STAmount amt2(mpt, 1500); + BEAST_EXPECT(canAdd(amt1, amt2) == true); + } + + // Adding two negative MPT amounts + { + STAmount amt1(mpt, -500); + STAmount amt2(mpt, -1500); + BEAST_EXPECT(canAdd(amt1, amt2) == true); + } + + // Adding a positive and a negative MPT amount + { + STAmount amt1(mpt, 1000); + STAmount amt2(mpt, -1000); + BEAST_EXPECT(canAdd(amt1, amt2) == true); + } + + // Overflow check for max MPT amounts + { + STAmount amt1( + mpt, std::numeric_limits::max()); + STAmount amt2(mpt, 1); + BEAST_EXPECT(canAdd(amt1, amt2) == false); + } + + // Overflow check for min MPT amounts + // Note: Cannot check min MPT overflow because you cannot initialize the + // STAmount with a negative MPT amount. + + // Adding MPT and XRP + { + STAmount amt1(XRPAmount(1000)); + STAmount amt2(mpt, 1000); + BEAST_EXPECT(canAdd(amt1, amt2) == false); + } + + // Adding different MPT issues (non zero) + { + STAmount amt1(mpt2, 500); + STAmount amt2(mpt, 500); + BEAST_EXPECT(canAdd(amt1, amt2) == false); + } + + // Adding different MPT issues (non zero) + { + STAmount amt1(mpt2, 0); + STAmount amt2(mpt, 500); + BEAST_EXPECT(canAdd(amt1, amt2) == false); + } + } + + void + testCanSubtractXRP() + { + testcase("can subtract xrp"); + + // Subtracting zero + { + STAmount amt1(XRPAmount(1000)); + STAmount amt2(XRPAmount(0)); + BEAST_EXPECT(canSubtract(amt1, amt2) == true); + } + + // Subtracting zero + { + STAmount amt1(XRPAmount(0)); + STAmount amt2(XRPAmount(1000)); + BEAST_EXPECT(canSubtract(amt1, amt2) == false); + } + + // Subtracting two positive XRP amounts + { + STAmount amt1(XRPAmount(1500)); + STAmount amt2(XRPAmount(500)); + BEAST_EXPECT(canSubtract(amt1, amt2) == true); + } + + // Subtracting two negative XRP amounts + { + STAmount amt1(XRPAmount(-1500)); + STAmount amt2(XRPAmount(-500)); + BEAST_EXPECT(canSubtract(amt1, amt2) == true); + } + + // Subtracting a positive and a negative XRP amount + { + STAmount amt1(XRPAmount(1000)); + STAmount amt2(XRPAmount(-1000)); + BEAST_EXPECT(canSubtract(amt1, amt2) == true); + } + + // Underflow check for min XRP amounts + { + STAmount amt1(std::numeric_limits::max()); + amt1 += XRPAmount(1); + STAmount amt2(XRPAmount(1)); + BEAST_EXPECT(canSubtract(amt1, amt2) == false); + } + + // Overflow check for max XRP amounts + { + STAmount amt1(std::numeric_limits::max()); + STAmount amt2(XRPAmount(-1)); + BEAST_EXPECT(canSubtract(amt1, amt2) == false); + } + } + + void + testCanSubtractIOU() + { + testcase("can subtract iou"); + Issue const usd{Currency(0x5553440000000000), AccountID(0x4985601)}; + Issue const eur{Currency(0x4555520000000000), AccountID(0x4985601)}; + + // Subtracting two IOU amounts + { + STAmount amt1(usd, 1500); + STAmount amt2(usd, 500); + BEAST_EXPECT(canSubtract(amt1, amt2) == true); + } + + // Subtracting XRP and IOU + { + STAmount amt1(XRPAmount(1000)); + STAmount amt2(usd, 1000); + BEAST_EXPECT(canSubtract(amt1, amt2) == false); + } + + // Subtracting different IOU issues (non zero) + { + STAmount amt1(usd, 1000); + STAmount amt2(eur, 500); + BEAST_EXPECT(canSubtract(amt1, amt2) == false); + } + + // Subtracting different IOU issues (zero) + { + STAmount amt1(usd, 0); + STAmount amt2(eur, 500); + BEAST_EXPECT(canSubtract(amt1, amt2) == false); + } + } + + void + testCanSubtractMPT() + { + testcase("can subtract mpt"); + + MPTIssue const mpt{MPTIssue{makeMptID(1, AccountID(0x4985601))}}; + MPTIssue const mpt2{MPTIssue{makeMptID(2, AccountID(0x4985601))}}; + + // Subtracting zero + { + STAmount amt1(mpt, 1000); + STAmount amt2(mpt, 0); + BEAST_EXPECT(canSubtract(amt1, amt2) == true); + } + + // Subtracting zero + { + STAmount amt1(mpt, 0); + STAmount amt2(mpt, 1000); + BEAST_EXPECT(canSubtract(amt1, amt2) == false); + } + + // Subtracting two positive MPT amounts + { + STAmount amt1(mpt, 1500); + STAmount amt2(mpt, 500); + BEAST_EXPECT(canSubtract(amt1, amt2) == true); + } + + // Subtracting two negative MPT amounts + { + STAmount amt1(mpt, -1500); + STAmount amt2(mpt, -500); + BEAST_EXPECT(canSubtract(amt1, amt2) == true); + } + + // Subtracting a positive and a negative MPT amount + { + STAmount amt1(mpt, 1000); + STAmount amt2(mpt, -1000); + BEAST_EXPECT(canSubtract(amt1, amt2) == true); + } + + // Underflow check for min MPT amounts + // Note: Cannot check min MPT underflow because you cannot initialize + // the STAmount with a negative MPT amount. + + // Overflow check for max positive MPT amounts (should fail) + { + STAmount amt1( + mpt, std::numeric_limits::max()); + STAmount amt2(mpt, -2); + BEAST_EXPECT(canSubtract(amt1, amt2) == false); + } + + // Subtracting MPT and XRP + { + STAmount amt1(XRPAmount(1000)); + STAmount amt2(mpt, 1000); + BEAST_EXPECT(canSubtract(amt1, amt2) == false); + } + + // Subtracting different MPT issues (non zero) + { + STAmount amt1(mpt, 1000); + STAmount amt2(mpt2, 500); + BEAST_EXPECT(canSubtract(amt1, amt2) == false); + } + + // Subtracting different MPT issues (zero) + { + STAmount amt1(mpt, 0); + STAmount amt2(mpt2, 500); + BEAST_EXPECT(canSubtract(amt1, amt2) == false); + } + } + //-------------------------------------------------------------------------- void @@ -681,6 +1043,12 @@ public: testRounding(); testConvertXRP(); testConvertIOU(); + testCanAddXRP(); + testCanAddIOU(); + testCanAddMPT(); + testCanSubtractXRP(); + testCanSubtractIOU(); + testCanSubtractMPT(); } }; diff --git a/src/test/rpc/AMMInfo_test.cpp b/src/test/rpc/AMMInfo_test.cpp index a0985ea104..a6e866b1c8 100644 --- a/src/test/rpc/AMMInfo_test.cpp +++ b/src/test/rpc/AMMInfo_test.cpp @@ -203,98 +203,119 @@ public: } void - testVoteAndBid() + testVoteAndBid(FeatureBitset features) { testcase("Vote and Bid"); using namespace jtx; - testAMM([&](AMM& ammAlice, Env& env) { - BEAST_EXPECT(ammAlice.expectAmmRpcInfo( - XRP(10000), USD(10000), IOUAmount{10000000, 0})); - std::unordered_map votes; - votes.insert({alice.human(), 0}); - for (int i = 0; i < 7; ++i) - { - Account a(std::to_string(i)); - votes.insert({a.human(), 50 * (i + 1)}); - fund(env, gw, {a}, {USD(10000)}, Fund::Acct); - ammAlice.deposit(a, 10000000); - ammAlice.vote(a, 50 * (i + 1)); - } - BEAST_EXPECT(ammAlice.expectTradingFee(175)); - Account ed("ed"); - Account bill("bill"); - env.fund(XRP(1000), bob, ed, bill); - env(ammAlice.bid( - {.bidMin = 100, .authAccounts = {carol, bob, ed, bill}})); - BEAST_EXPECT(ammAlice.expectAmmRpcInfo( - XRP(80000), - USD(80000), - IOUAmount{79994400}, - std::nullopt, - std::nullopt, - ammAlice.ammAccount())); - for (auto i = 0; i < 2; ++i) - { - std::unordered_set authAccounts = { - carol.human(), bob.human(), ed.human(), bill.human()}; - auto const ammInfo = i ? ammAlice.ammRpcInfo() - : ammAlice.ammRpcInfo( - std::nullopt, - std::nullopt, - std::nullopt, - std::nullopt, - ammAlice.ammAccount()); - auto const& amm = ammInfo[jss::amm]; - try + testAMM( + [&](AMM& ammAlice, Env& env) { + BEAST_EXPECT(ammAlice.expectAmmRpcInfo( + XRP(10000), USD(10000), IOUAmount{10000000, 0})); + std::unordered_map votes; + votes.insert({alice.human(), 0}); + for (int i = 0; i < 7; ++i) { - // votes - auto const voteSlots = amm[jss::vote_slots]; - auto votesCopy = votes; - for (std::uint8_t i = 0; i < 8; ++i) + Account a(std::to_string(i)); + votes.insert({a.human(), 50 * (i + 1)}); + if (!features[fixAMMv1_3]) + fund(env, gw, {a}, {USD(10000)}, Fund::Acct); + else + fund(env, gw, {a}, {USD(10001)}, Fund::Acct); + ammAlice.deposit(a, 10000000); + ammAlice.vote(a, 50 * (i + 1)); + } + BEAST_EXPECT(ammAlice.expectTradingFee(175)); + Account ed("ed"); + Account bill("bill"); + env.fund(XRP(1000), bob, ed, bill); + env(ammAlice.bid( + {.bidMin = 100, .authAccounts = {carol, bob, ed, bill}})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(ammAlice.expectAmmRpcInfo( + XRP(80000), + USD(80000), + IOUAmount{79994400}, + std::nullopt, + std::nullopt, + ammAlice.ammAccount())); + else + BEAST_EXPECT(ammAlice.expectAmmRpcInfo( + XRPAmount(80000000005), + STAmount{USD, UINT64_C(80'000'00000000005), -11}, + IOUAmount{79994400}, + std::nullopt, + std::nullopt, + ammAlice.ammAccount())); + for (auto i = 0; i < 2; ++i) + { + std::unordered_set authAccounts = { + carol.human(), bob.human(), ed.human(), bill.human()}; + auto const ammInfo = i ? ammAlice.ammRpcInfo() + : ammAlice.ammRpcInfo( + std::nullopt, + std::nullopt, + std::nullopt, + std::nullopt, + ammAlice.ammAccount()); + auto const& amm = ammInfo[jss::amm]; + try { - if (!BEAST_EXPECT( - votes[voteSlots[i][jss::account].asString()] == - voteSlots[i][jss::trading_fee].asUInt() && - voteSlots[i][jss::vote_weight].asUInt() == - 12500)) + // votes + auto const voteSlots = amm[jss::vote_slots]; + auto votesCopy = votes; + for (std::uint8_t i = 0; i < 8; ++i) + { + if (!BEAST_EXPECT( + votes[voteSlots[i][jss::account] + .asString()] == + voteSlots[i][jss::trading_fee] + .asUInt() && + voteSlots[i][jss::vote_weight].asUInt() == + 12500)) + return; + votes.erase(voteSlots[i][jss::account].asString()); + } + if (!BEAST_EXPECT(votes.empty())) return; - votes.erase(voteSlots[i][jss::account].asString()); - } - if (!BEAST_EXPECT(votes.empty())) - return; - votes = votesCopy; + votes = votesCopy; - // bid - auto const auctionSlot = amm[jss::auction_slot]; - for (std::uint8_t i = 0; i < 4; ++i) - { - if (!BEAST_EXPECT(authAccounts.contains( + // bid + auto const auctionSlot = amm[jss::auction_slot]; + for (std::uint8_t i = 0; i < 4; ++i) + { + if (!BEAST_EXPECT(authAccounts.contains( + auctionSlot[jss::auth_accounts][i] + [jss::account] + .asString()))) + return; + authAccounts.erase( auctionSlot[jss::auth_accounts][i][jss::account] - .asString()))) + .asString()); + } + if (!BEAST_EXPECT(authAccounts.empty())) return; - authAccounts.erase( - auctionSlot[jss::auth_accounts][i][jss::account] - .asString()); + BEAST_EXPECT( + auctionSlot[jss::account].asString() == + alice.human() && + auctionSlot[jss::discounted_fee].asUInt() == 17 && + auctionSlot[jss::price][jss::value].asString() == + "5600" && + auctionSlot[jss::price][jss::currency].asString() == + to_string(ammAlice.lptIssue().currency) && + auctionSlot[jss::price][jss::issuer].asString() == + to_string(ammAlice.lptIssue().account)); + } + catch (std::exception const& e) + { + fail(e.what(), __FILE__, __LINE__); } - if (!BEAST_EXPECT(authAccounts.empty())) - return; - BEAST_EXPECT( - auctionSlot[jss::account].asString() == alice.human() && - auctionSlot[jss::discounted_fee].asUInt() == 17 && - auctionSlot[jss::price][jss::value].asString() == - "5600" && - auctionSlot[jss::price][jss::currency].asString() == - to_string(ammAlice.lptIssue().currency) && - auctionSlot[jss::price][jss::issuer].asString() == - to_string(ammAlice.lptIssue().account)); } - catch (std::exception const& e) - { - fail(e.what(), __FILE__, __LINE__); - } - } - }); + }, + std::nullopt, + 0, + std::nullopt, + {features}); } void @@ -337,9 +358,12 @@ public: void run() override { + using namespace jtx; + auto const all = testable_amendments(); testErrors(); testSimpleRpc(); - testVoteAndBid(); + testVoteAndBid(all); + testVoteAndBid(all - fixAMMv1_3); testFreeze(); testInvalidAmmField(); } diff --git a/src/test/rpc/AccountInfo_test.cpp b/src/test/rpc/AccountInfo_test.cpp index 238b739611..18c8bf5a1c 100644 --- a/src/test/rpc/AccountInfo_test.cpp +++ b/src/test/rpc/AccountInfo_test.cpp @@ -675,6 +675,30 @@ public: BEAST_EXPECT( !getAccountFlag(allowTrustLineClawbackFlag.first, bob)); } + + static constexpr std::pair + allowTrustLineLockingFlag{ + "allowTrustLineLocking", asfAllowTrustLineLocking}; + + if (features[featureTokenEscrow]) + { + auto const f1 = + getAccountFlag(allowTrustLineLockingFlag.first, bob); + BEAST_EXPECT(f1.has_value()); + BEAST_EXPECT(!f1.value()); + + // Set allowTrustLineLocking + env(fset(bob, allowTrustLineLockingFlag.second)); + env.close(); + auto const f2 = + getAccountFlag(allowTrustLineLockingFlag.first, bob); + BEAST_EXPECT(f2.has_value()); + BEAST_EXPECT(f2.value()); + } + else + { + BEAST_EXPECT(!getAccountFlag(allowTrustLineLockingFlag.first, bob)); + } } void @@ -686,11 +710,14 @@ public: testSignerListsV2(); FeatureBitset const allFeatures{ - ripple::test::jtx::supported_amendments()}; + ripple::test::jtx::testable_amendments()}; testAccountFlags(allFeatures); testAccountFlags(allFeatures - featureDisallowIncoming); testAccountFlags( allFeatures - featureDisallowIncoming - featureClawback); + testAccountFlags( + allFeatures - featureDisallowIncoming - featureClawback - + featureTokenEscrow); } }; diff --git a/src/test/rpc/AccountLines_test.cpp b/src/test/rpc/AccountLines_test.cpp index 6e6f0def19..9215f4087a 100644 --- a/src/test/rpc/AccountLines_test.cpp +++ b/src/test/rpc/AccountLines_test.cpp @@ -573,22 +573,6 @@ public: env.fund(XRP(10000), alice, becky, gw1); env.close(); - // A couple of helper lambdas - auto escrow = [&env]( - Account const& account, - Account const& to, - STAmount const& amount) { - Json::Value jv; - jv[jss::TransactionType] = jss::EscrowCreate; - jv[jss::Flags] = tfUniversal; - jv[jss::Account] = account.human(); - jv[jss::Destination] = to.human(); - jv[jss::Amount] = amount.getJson(JsonOptions::none); - NetClock::time_point finish = env.now() + 1s; - jv[sfFinishAfter.jsonName] = finish.time_since_epoch().count(); - return jv; - }; - auto payChan = [](Account const& account, Account const& to, STAmount const& amount, @@ -596,7 +580,6 @@ public: PublicKey const& pk) { Json::Value jv; jv[jss::TransactionType] = jss::PaymentChannelCreate; - jv[jss::Flags] = tfUniversal; jv[jss::Account] = account.human(); jv[jss::Destination] = to.human(); jv[jss::Amount] = amount.getJson(JsonOptions::none); @@ -625,8 +608,10 @@ public: env.close(); // Escrow, in each direction - env(escrow(alice, becky, XRP(1000))); - env(escrow(becky, alice, XRP(1000))); + env(escrow::create(alice, becky, XRP(1000)), + escrow::finish_time(env.now() + 1s)); + env(escrow::create(becky, alice, XRP(1000)), + escrow::finish_time(env.now() + 1s)); // Pay channels, in each direction env(payChan(alice, becky, XRP(1000), 100s, alice.pk())); diff --git a/src/test/rpc/AccountObjects_test.cpp b/src/test/rpc/AccountObjects_test.cpp index b723095aeb..546bbe8715 100644 --- a/src/test/rpc/AccountObjects_test.cpp +++ b/src/test/rpc/AccountObjects_test.cpp @@ -577,7 +577,7 @@ public: Account const gw{"gateway"}; auto const USD = gw["USD"]; - auto const features = supported_amendments() | featureXChainBridge | + auto const features = testable_amendments() | featureXChainBridge | featurePermissionedDomains; Env env(*this, features); @@ -698,7 +698,6 @@ public: // gw creates an escrow that we can look for in the ledger. Json::Value jvEscrow; jvEscrow[jss::TransactionType] = jss::EscrowCreate; - jvEscrow[jss::Flags] = tfUniversal; jvEscrow[jss::Account] = gw.human(); jvEscrow[jss::Destination] = gw.human(); jvEscrow[jss::Amount] = XRP(100).value().getJson(JsonOptions::none); @@ -912,7 +911,6 @@ public: // for. Json::Value jvPayChan; jvPayChan[jss::TransactionType] = jss::PaymentChannelCreate; - jvPayChan[jss::Flags] = tfUniversal; jvPayChan[jss::Account] = gw.human(); jvPayChan[jss::Destination] = alice.human(); jvPayChan[jss::Amount] = @@ -938,7 +936,6 @@ public: // gw creates a DID that we can look for in the ledger. Json::Value jvDID; jvDID[jss::TransactionType] = jss::DIDSet; - jvDID[jss::Flags] = tfUniversal; jvDID[jss::Account] = gw.human(); jvDID[sfURI.jsonName] = strHex(std::string{"uri"}); env(jvDID); diff --git a/src/test/rpc/AccountSet_test.cpp b/src/test/rpc/AccountSet_test.cpp index 7bca51ae96..5c0ca89305 100644 --- a/src/test/rpc/AccountSet_test.cpp +++ b/src/test/rpc/AccountSet_test.cpp @@ -53,7 +53,7 @@ public: Account const alice("alice"); // Test without DepositAuth enabled initially. - Env env(*this, supported_amendments() - featureDepositAuth); + Env env(*this, testable_amendments() - featureDepositAuth); env.fund(XRP(10000), noripple(alice)); // Give alice a regular key so she can legally set and clear @@ -99,6 +99,12 @@ public: // is tested elsewhere. continue; } + if (flag == asfAllowTrustLineLocking) + { + // These flags are part of the AllowTokenLocking amendment + // and are tested elsewhere + continue; + } if (std::find(goodFlags.begin(), goodFlags.end(), flag) != goodFlags.end()) @@ -351,7 +357,7 @@ public: }; doTests( - supported_amendments(), + testable_amendments(), {{1.0, tesSUCCESS, 1.0}, {1.1, tesSUCCESS, 1.1}, {2.0, tesSUCCESS, 2.0}, diff --git a/src/test/rpc/AccountTx_test.cpp b/src/test/rpc/AccountTx_test.cpp index 9af3fdcb61..82809b5c5b 100644 --- a/src/test/rpc/AccountTx_test.cpp +++ b/src/test/rpc/AccountTx_test.cpp @@ -18,9 +18,12 @@ //============================================================================== #include +#include #include +#include #include +#include #include #include @@ -458,7 +461,6 @@ class AccountTx_test : public beast::unit_test::suite STAmount const& amount) { Json::Value escro; escro[jss::TransactionType] = jss::EscrowCreate; - escro[jss::Flags] = tfUniversal; escro[jss::Account] = account.human(); escro[jss::Destination] = to.human(); escro[jss::Amount] = amount.getJson(JsonOptions::none); @@ -487,7 +489,6 @@ class AccountTx_test : public beast::unit_test::suite { Json::Value escrowFinish; escrowFinish[jss::TransactionType] = jss::EscrowFinish; - escrowFinish[jss::Flags] = tfUniversal; escrowFinish[jss::Account] = alice.human(); escrowFinish[sfOwner.jsonName] = alice.human(); escrowFinish[sfOfferSequence.jsonName] = escrowFinishSeq; @@ -496,7 +497,6 @@ class AccountTx_test : public beast::unit_test::suite { Json::Value escrowCancel; escrowCancel[jss::TransactionType] = jss::EscrowCancel; - escrowCancel[jss::Flags] = tfUniversal; escrowCancel[jss::Account] = alice.human(); escrowCancel[sfOwner.jsonName] = alice.human(); escrowCancel[sfOfferSequence.jsonName] = escrowCancelSeq; @@ -510,7 +510,6 @@ class AccountTx_test : public beast::unit_test::suite std::uint32_t payChanSeq{env.seq(alice)}; Json::Value payChanCreate; payChanCreate[jss::TransactionType] = jss::PaymentChannelCreate; - payChanCreate[jss::Flags] = tfUniversal; payChanCreate[jss::Account] = alice.human(); payChanCreate[jss::Destination] = gw.human(); payChanCreate[jss::Amount] = @@ -527,7 +526,6 @@ class AccountTx_test : public beast::unit_test::suite { Json::Value payChanFund; payChanFund[jss::TransactionType] = jss::PaymentChannelFund; - payChanFund[jss::Flags] = tfUniversal; payChanFund[jss::Account] = alice.human(); payChanFund[sfChannel.jsonName] = payChanIndex; payChanFund[jss::Amount] = @@ -758,6 +756,85 @@ class AccountTx_test : public beast::unit_test::suite } } + void + testMPT() + { + testcase("MPT"); + + using namespace test::jtx; + using namespace std::chrono_literals; + + auto cfg = makeConfig(); + cfg->FEES.reference_fee = 10; + Env env(*this, std::move(cfg)); + + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const carol{"carol"}; + + MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); + + // check the latest mpt-related txn is in alice's account history + auto const checkAliceAcctTx = [&](size_t size, + Json::StaticString txType) { + Json::Value params; + params[jss::account] = alice.human(); + params[jss::limit] = 100; + auto const jv = + env.rpc("json", "account_tx", to_string(params))[jss::result]; + + BEAST_EXPECT(jv[jss::transactions].size() == size); + auto const& tx0(jv[jss::transactions][0u][jss::tx]); + BEAST_EXPECT(tx0[jss::TransactionType] == txType); + + std::string const txHash{ + env.tx()->getJson(JsonOptions::none)[jss::hash].asString()}; + BEAST_EXPECT(tx0[jss::hash] == txHash); + }; + + // alice creates issuance + mptAlice.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanClawback | tfMPTRequireAuth | tfMPTCanTransfer}); + + checkAliceAcctTx(3, jss::MPTokenIssuanceCreate); + + // bob creates a MPToken; + mptAlice.authorize({.account = bob}); + checkAliceAcctTx(4, jss::MPTokenAuthorize); + env.close(); + + // TODO: windows pipeline fails validation for the hardcoded ledger hash + // due to having different test config, it can be uncommented after + // figuring out what happened + // + // ledger hash should be fixed regardless any change to account history + // BEAST_EXPECT( + // to_string(env.closed()->info().hash) == + // "0BD507BB87D3C0E73B462485E6E381798A8C82FC49BF17FE39C60E08A1AF035D"); + + // alice authorizes bob + mptAlice.authorize({.account = alice, .holder = bob}); + checkAliceAcctTx(5, jss::MPTokenAuthorize); + + // carol creates a MPToken; + mptAlice.authorize({.account = carol}); + checkAliceAcctTx(6, jss::MPTokenAuthorize); + + // alice authorizes carol + mptAlice.authorize({.account = alice, .holder = carol}); + checkAliceAcctTx(7, jss::MPTokenAuthorize); + + // alice pays bob 100 tokens + mptAlice.pay(alice, bob, 100); + checkAliceAcctTx(8, jss::Payment); + + // bob pays carol 10 tokens + mptAlice.pay(bob, carol, 10); + checkAliceAcctTx(9, jss::Payment); + } + public: void run() override @@ -766,6 +843,7 @@ public: std::bind_front(&AccountTx_test::testParameters, this)); testContents(); testAccountDelete(); + testMPT(); } }; BEAST_DEFINE_TESTSUITE(AccountTx, rpc, ripple); diff --git a/src/test/rpc/BookChanges_test.cpp b/src/test/rpc/BookChanges_test.cpp index 95997538d7..1f7b6775f2 100644 --- a/src/test/rpc/BookChanges_test.cpp +++ b/src/test/rpc/BookChanges_test.cpp @@ -18,6 +18,10 @@ //============================================================================== #include +#include + +#include "xrpl/beast/unit_test/suite.h" +#include "xrpl/protocol/jss.h" namespace ripple { namespace test { @@ -83,14 +87,59 @@ public: // == 3); } + void + testDomainOffer() + { + testcase("Domain Offer"); + using namespace jtx; + + FeatureBitset const all{ + jtx::testable_amendments() | featurePermissionedDomains | + featureCredentials | featurePermissionedDEX}; + + Env env(*this, all); + PermissionedDEX permDex(env); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + permDex; + + auto wsc = makeWSClient(env.app().config()); + + env(offer(alice, XRP(10), USD(10)), domain(domainID)); + env.close(); + + env(pay(bob, carol, USD(10)), + path(~USD), + sendmax(XRP(10)), + domain(domainID)); + env.close(); + + std::string const txHash{ + env.tx()->getJson(JsonOptions::none)[jss::hash].asString()}; + + Json::Value const txResult = env.rpc("tx", txHash)[jss::result]; + auto const ledgerIndex = txResult[jss::ledger_index].asInt(); + + Json::Value jvParams; + jvParams[jss::ledger_index] = ledgerIndex; + + auto jv = wsc->invoke("book_changes", jvParams); + auto jrr = jv[jss::result]; + + BEAST_EXPECT(jrr[jss::changes].size() == 1); + BEAST_EXPECT( + jrr[jss::changes][0u][jss::domain].asString() == + to_string(domainID)); + } + void run() override { testConventionalLedgerInputStrings(); testLedgerInputDefaultBehavior(); - // Note: Other aspects of the book_changes rpc are fertile grounds for - // unit-testing purposes. It can be included in future work + testDomainOffer(); + // Note: Other aspects of the book_changes rpc are fertile grounds + // for unit-testing purposes. It can be included in future work } }; diff --git a/src/test/rpc/Book_test.cpp b/src/test/rpc/Book_test.cpp index 79e3f940f8..e885762644 100644 --- a/src/test/rpc/Book_test.cpp +++ b/src/test/rpc/Book_test.cpp @@ -22,6 +22,8 @@ #include #include +#include +#include #include namespace ripple { @@ -30,10 +32,14 @@ namespace test { class Book_test : public beast::unit_test::suite { std::string - getBookDir(jtx::Env& env, Issue const& in, Issue const& out) + getBookDir( + jtx::Env& env, + Issue const& in, + Issue const& out, + std::optional const& domain = std::nullopt) { std::string dir; - auto uBookBase = getBookBase({in, out}); + auto uBookBase = getBookBase({in, out, domain}); auto uBookEnd = getQualityNext(uBookBase); auto view = env.closed(); auto key = view->succ(uBookBase, uBookEnd); @@ -1657,6 +1663,19 @@ public: "Unneeded field 'taker_gets.issuer' " "for XRP currency specification."); } + { + Json::Value jvParams; + jvParams[jss::ledger_index] = "validated"; + jvParams[jss::taker_pays][jss::currency] = "USD"; + jvParams[jss::taker_pays][jss::issuer] = gw.human(); + jvParams[jss::taker_gets][jss::currency] = "EUR"; + jvParams[jss::taker_gets][jss::issuer] = gw.human(); + jvParams[jss::domain] = "badString"; + auto const jrr = env.rpc( + "json", "book_offers", to_string(jvParams))[jss::result]; + BEAST_EXPECT(jrr[jss::error] == "domainMalformed"); + BEAST_EXPECT(jrr[jss::error_message] == "Unable to parse domain."); + } } void @@ -1711,6 +1730,273 @@ public: (asAdmin ? RPC::Tuning::bookOffers.rdefault : 0u)); } + void + testTrackDomainOffer() + { + testcase("TrackDomainOffer"); + using namespace jtx; + + FeatureBitset const all{ + jtx::testable_amendments() | featurePermissionedDomains | + featureCredentials | featurePermissionedDEX}; + + Env env(*this, all); + PermissionedDEX permDex(env); + auto const alice = permDex.alice; + auto const bob = permDex.bob; + auto const carol = permDex.carol; + auto const domainID = permDex.domainID; + auto const gw = permDex.gw; + auto const USD = permDex.USD; + + auto wsc = makeWSClient(env.app().config()); + + env(offer(alice, XRP(10), USD(10)), domain(domainID)); + env.close(); + + auto checkBookOffers = [&](Json::Value const& jrr) { + BEAST_EXPECT(jrr[jss::offers].isArray()); + BEAST_EXPECT(jrr[jss::offers].size() == 1); + auto const jrOffer = jrr[jss::offers][0u]; + BEAST_EXPECT(jrOffer[sfAccount.fieldName] == alice.human()); + BEAST_EXPECT( + jrOffer[sfBookDirectory.fieldName] == + getBookDir(env, XRP, USD.issue(), domainID)); + BEAST_EXPECT(jrOffer[sfBookNode.fieldName] == "0"); + BEAST_EXPECT(jrOffer[jss::Flags] == 0); + BEAST_EXPECT(jrOffer[sfLedgerEntryType.fieldName] == jss::Offer); + BEAST_EXPECT(jrOffer[sfOwnerNode.fieldName] == "0"); + BEAST_EXPECT( + jrOffer[jss::TakerGets] == + USD(10).value().getJson(JsonOptions::none)); + BEAST_EXPECT( + jrOffer[jss::TakerPays] == + XRP(10).value().getJson(JsonOptions::none)); + BEAST_EXPECT( + jrOffer[sfDomainID.jsonName].asString() == to_string(domainID)); + }; + + // book_offers: open book doesn't return offer + { + Json::Value jvParams; + jvParams[jss::taker] = env.master.human(); + jvParams[jss::taker_pays][jss::currency] = "XRP"; + jvParams[jss::ledger_index] = "validated"; + jvParams[jss::taker_gets][jss::currency] = "USD"; + jvParams[jss::taker_gets][jss::issuer] = gw.human(); + + auto jv = wsc->invoke("book_offers", jvParams); + auto jrr = jv[jss::result]; + BEAST_EXPECT(jrr[jss::offers].isArray()); + BEAST_EXPECT(jrr[jss::offers].size() == 0); + } + + auto checkSubBooks = [&](Json::Value const& jv) { + BEAST_EXPECT( + jv[jss::result].isMember(jss::offers) && + jv[jss::result][jss::offers].size() == 1); + BEAST_EXPECT( + jv[jss::result][jss::offers][0u][jss::TakerGets] == + USD(10).value().getJson(JsonOptions::none)); + BEAST_EXPECT( + jv[jss::result][jss::offers][0u][jss::TakerPays] == + XRP(10).value().getJson(JsonOptions::none)); + BEAST_EXPECT( + jv[jss::result][jss::offers][0u][sfDomainID.jsonName] + .asString() == to_string(domainID)); + }; + + // book_offers: requesting domain book returns hybrid offer + { + Json::Value jvParams; + jvParams[jss::taker] = env.master.human(); + jvParams[jss::taker_pays][jss::currency] = "XRP"; + jvParams[jss::ledger_index] = "validated"; + jvParams[jss::taker_gets][jss::currency] = "USD"; + jvParams[jss::taker_gets][jss::issuer] = gw.human(); + jvParams[jss::domain] = to_string(domainID); + + auto jv = wsc->invoke("book_offers", jvParams); + auto jrr = jv[jss::result]; + checkBookOffers(jrr); + } + + // subscribe to domain book should return domain offer + { + Json::Value books; + books[jss::books] = Json::arrayValue; + { + auto& j = books[jss::books].append(Json::objectValue); + j[jss::snapshot] = true; + j[jss::taker_pays][jss::currency] = "XRP"; + j[jss::taker_gets][jss::currency] = "USD"; + j[jss::taker_gets][jss::issuer] = gw.human(); + j[jss::domain] = to_string(domainID); + } + + auto jv = wsc->invoke("subscribe", books); + if (!BEAST_EXPECT(jv[jss::status] == "success")) + return; + checkSubBooks(jv); + } + + // subscribe to open book should not return domain offer + { + Json::Value books; + books[jss::books] = Json::arrayValue; + { + auto& j = books[jss::books].append(Json::objectValue); + j[jss::snapshot] = true; + j[jss::taker_pays][jss::currency] = "XRP"; + j[jss::taker_gets][jss::currency] = "USD"; + j[jss::taker_gets][jss::issuer] = gw.human(); + } + + auto jv = wsc->invoke("subscribe", books); + if (!BEAST_EXPECT(jv[jss::status] == "success")) + return; + BEAST_EXPECT( + jv[jss::result].isMember(jss::offers) && + jv[jss::result][jss::offers].size() == 0); + } + } + + void + testTrackHybridOffer() + { + testcase("TrackHybridOffer"); + using namespace jtx; + + FeatureBitset const all{ + jtx::testable_amendments() | featurePermissionedDomains | + featureCredentials | featurePermissionedDEX}; + + Env env(*this, all); + PermissionedDEX permDex(env); + auto const alice = permDex.alice; + auto const bob = permDex.bob; + auto const carol = permDex.carol; + auto const domainID = permDex.domainID; + auto const gw = permDex.gw; + auto const USD = permDex.USD; + + auto wsc = makeWSClient(env.app().config()); + + env(offer(alice, XRP(10), USD(10)), + domain(domainID), + txflags(tfHybrid)); + env.close(); + + auto checkBookOffers = [&](Json::Value const& jrr) { + BEAST_EXPECT(jrr[jss::offers].isArray()); + BEAST_EXPECT(jrr[jss::offers].size() == 1); + auto const jrOffer = jrr[jss::offers][0u]; + BEAST_EXPECT(jrOffer[sfAccount.fieldName] == alice.human()); + BEAST_EXPECT( + jrOffer[sfBookDirectory.fieldName] == + getBookDir(env, XRP, USD.issue(), domainID)); + BEAST_EXPECT(jrOffer[sfBookNode.fieldName] == "0"); + BEAST_EXPECT(jrOffer[jss::Flags] == lsfHybrid); + BEAST_EXPECT(jrOffer[sfLedgerEntryType.fieldName] == jss::Offer); + BEAST_EXPECT(jrOffer[sfOwnerNode.fieldName] == "0"); + BEAST_EXPECT( + jrOffer[jss::TakerGets] == + USD(10).value().getJson(JsonOptions::none)); + BEAST_EXPECT( + jrOffer[jss::TakerPays] == + XRP(10).value().getJson(JsonOptions::none)); + BEAST_EXPECT( + jrOffer[sfDomainID.jsonName].asString() == to_string(domainID)); + BEAST_EXPECT(jrOffer[sfAdditionalBooks.jsonName].size() == 1); + }; + + // book_offers: open book returns hybrid offer + { + Json::Value jvParams; + jvParams[jss::taker] = env.master.human(); + jvParams[jss::taker_pays][jss::currency] = "XRP"; + jvParams[jss::ledger_index] = "validated"; + jvParams[jss::taker_gets][jss::currency] = "USD"; + jvParams[jss::taker_gets][jss::issuer] = gw.human(); + + auto jv = wsc->invoke("book_offers", jvParams); + auto jrr = jv[jss::result]; + checkBookOffers(jrr); + } + + auto checkSubBooks = [&](Json::Value const& jv) { + BEAST_EXPECT( + jv[jss::result].isMember(jss::offers) && + jv[jss::result][jss::offers].size() == 1); + BEAST_EXPECT( + jv[jss::result][jss::offers][0u][jss::TakerGets] == + USD(10).value().getJson(JsonOptions::none)); + BEAST_EXPECT( + jv[jss::result][jss::offers][0u][jss::TakerPays] == + XRP(10).value().getJson(JsonOptions::none)); + BEAST_EXPECT( + jv[jss::result][jss::offers][0u][sfDomainID.jsonName] + .asString() == to_string(domainID)); + }; + + // book_offers: requesting domain book returns hybrid offer + { + Json::Value jvParams; + jvParams[jss::taker] = env.master.human(); + jvParams[jss::taker_pays][jss::currency] = "XRP"; + jvParams[jss::ledger_index] = "validated"; + jvParams[jss::taker_gets][jss::currency] = "USD"; + jvParams[jss::taker_gets][jss::issuer] = gw.human(); + jvParams[jss::domain] = to_string(domainID); + + auto jv = wsc->invoke("book_offers", jvParams); + auto jrr = jv[jss::result]; + checkBookOffers(jrr); + } + + // subscribe to domain book should return hybrid offer + { + Json::Value books; + books[jss::books] = Json::arrayValue; + { + auto& j = books[jss::books].append(Json::objectValue); + j[jss::snapshot] = true; + j[jss::taker_pays][jss::currency] = "XRP"; + j[jss::taker_gets][jss::currency] = "USD"; + j[jss::taker_gets][jss::issuer] = gw.human(); + j[jss::domain] = to_string(domainID); + } + + auto jv = wsc->invoke("subscribe", books); + if (!BEAST_EXPECT(jv[jss::status] == "success")) + return; + checkSubBooks(jv); + + // RPC unsubscribe + auto unsubJv = wsc->invoke("unsubscribe", books); + if (wsc->version() == 2) + BEAST_EXPECT(unsubJv[jss::status] == "success"); + } + + // subscribe to open book should return hybrid offer + { + Json::Value books; + books[jss::books] = Json::arrayValue; + { + auto& j = books[jss::books].append(Json::objectValue); + j[jss::snapshot] = true; + j[jss::taker_pays][jss::currency] = "XRP"; + j[jss::taker_gets][jss::currency] = "USD"; + j[jss::taker_gets][jss::issuer] = gw.human(); + } + + auto jv = wsc->invoke("subscribe", books); + if (!BEAST_EXPECT(jv[jss::status] == "success")) + return; + checkSubBooks(jv); + } + } + void run() override { @@ -1728,6 +2014,8 @@ public: testBookOfferErrors(); testBookOfferLimits(true); testBookOfferLimits(false); + testTrackDomainOffer(); + testTrackHybridOffer(); } }; diff --git a/src/test/rpc/Feature_test.cpp b/src/test/rpc/Feature_test.cpp index 40de395a71..06697f80c1 100644 --- a/src/test/rpc/Feature_test.cpp +++ b/src/test/rpc/Feature_test.cpp @@ -139,7 +139,8 @@ class Feature_test : public beast::unit_test::suite // Test a random sampling of the variables. If any of these get retired // or removed, swap out for any other feature. - BEAST_EXPECT(featureToName(featureOwnerPaysFee) == "OwnerPaysFee"); + BEAST_EXPECT( + featureToName(fixTrustLinesToSelf) == "fixTrustLinesToSelf"); BEAST_EXPECT(featureToName(featureFlow) == "Flow"); BEAST_EXPECT(featureToName(featureNegativeUNL) == "NegativeUNL"); BEAST_EXPECT(featureToName(fix1578) == "fix1578"); diff --git a/src/test/rpc/GatewayBalances_test.cpp b/src/test/rpc/GatewayBalances_test.cpp index 249d4f892f..691f32317e 100644 --- a/src/test/rpc/GatewayBalances_test.cpp +++ b/src/test/rpc/GatewayBalances_test.cpp @@ -251,8 +251,8 @@ public: run() override { using namespace jtx; - auto const sa = supported_amendments(); - for (auto feature : {sa - featureFlowCross, sa}) + auto const sa = testable_amendments(); + for (auto feature : {sa - featurePermissionedDEX, sa}) { testGWB(feature); testGWBApiVersions(feature); diff --git a/src/test/rpc/JSONRPC_test.cpp b/src/test/rpc/JSONRPC_test.cpp index cd26758c1f..1612d1b455 100644 --- a/src/test/rpc/JSONRPC_test.cpp +++ b/src/test/rpc/JSONRPC_test.cpp @@ -2041,6 +2041,28 @@ static constexpr TxnTestData txnTestArray[] = { "Cannot specify differing 'Amount' and 'DeliverMax'", "Cannot specify differing 'Amount' and 'DeliverMax'", "Cannot specify differing 'Amount' and 'DeliverMax'"}}}, + {"Payment cannot specify bad DomainID.", + __LINE__, + R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "secret": "masterpassphrase", + "debug_signing": 0, + "tx_json": { + "Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "Amount": "1000000000", + "Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + "Fee": 50, + "Sequence": 0, + "SigningPubKey": "", + "TransactionType": "Payment", + "DomainID": "invalid", + } +})", + {{"Unable to parse 'DomainID'.", + "Unable to parse 'DomainID'.", + "Unable to parse 'DomainID'.", + "Unable to parse 'DomainID'."}}}, {"Minimal delegated transaction.", __LINE__, @@ -2132,6 +2154,127 @@ public: result[jss::result][jss::request][jss::command] == "bad_command"); } + void + testAutoFillFails() + { + testcase("autofill fails"); + using namespace test::jtx; + + // test batch raw transactions max size + { + Env env(*this); + auto ledger = env.current(); + auto const& feeTrack = env.app().getFeeTrack(); + Json::Value req; + Account const alice("alice"); + Account const bob("bob"); + env.fund(XRP(100000), alice); + env.close(); + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const seq = env.seq(alice); + auto jt = env.jtnofill( + batch::outer(alice, env.seq(alice), batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(2)), seq + 2), + batch::inner(pay(alice, bob, XRP(3)), seq + 3), + batch::inner(pay(alice, bob, XRP(4)), seq + 4), + batch::inner(pay(alice, bob, XRP(5)), seq + 5), + batch::inner(pay(alice, bob, XRP(6)), seq + 6), + batch::inner(pay(alice, bob, XRP(7)), seq + 7), + batch::inner(pay(alice, bob, XRP(8)), seq + 8), + batch::inner(pay(alice, bob, XRP(9)), seq + 9)); + + jt.jv.removeMember(jss::Fee); + jt.jv.removeMember(jss::TxnSignature); + req[jss::tx_json] = jt.jv; + Json::Value result = checkFee( + req, + Role::ADMIN, + true, + env.app().config(), + feeTrack, + env.app().getTxQ(), + env.app()); + BEAST_EXPECT(result.size() == 0); + BEAST_EXPECT( + req[jss::tx_json].isMember(jss::Fee) && + req[jss::tx_json][jss::Fee] == + env.current()->fees().base.jsonClipped()); + } + + // test signers max size + { + Env env(*this); + auto ledger = env.current(); + auto const& feeTrack = env.app().getFeeTrack(); + Json::Value req; + Account const alice("alice"); + Account const bob("bob"); + env.fund(XRP(100000), alice, bob); + env.close(); + + auto jt = env.jtnofill( + noop(alice), + msig( + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice)); + + req[jss::tx_json] = jt.jv; + Json::Value result = checkFee( + req, + Role::ADMIN, + true, + env.app().config(), + feeTrack, + env.app().getTxQ(), + env.app()); + BEAST_EXPECT(result.size() == 0); + BEAST_EXPECT( + req[jss::tx_json].isMember(jss::Fee) && + req[jss::tx_json][jss::Fee] == + env.current()->fees().base.jsonClipped()); + } + } + void testAutoFillFees() { @@ -2785,6 +2928,7 @@ public: run() override { testBadRpcCommand(); + testAutoFillFails(); testAutoFillFees(); testAutoFillEscalatedFees(); testAutoFillNetworkID(); diff --git a/src/test/rpc/LedgerData_test.cpp b/src/test/rpc/LedgerData_test.cpp index b56cb241dd..54f51255d1 100644 --- a/src/test/rpc/LedgerData_test.cpp +++ b/src/test/rpc/LedgerData_test.cpp @@ -304,8 +304,8 @@ public: // Make sure fixInnerObjTemplate2 doesn't break amendments. for (FeatureBitset const& features : - {supported_amendments() - fixInnerObjTemplate2, - supported_amendments() | fixInnerObjTemplate2}) + {testable_amendments() - fixInnerObjTemplate2, + testable_amendments() | fixInnerObjTemplate2}) { using namespace std::chrono; Env env{*this, envconfig(validator, ""), features}; @@ -369,7 +369,6 @@ public: { Json::Value jv; jv[jss::TransactionType] = jss::EscrowCreate; - jv[jss::Flags] = tfUniversal; jv[jss::Account] = Account{"bob5"}.human(); jv[jss::Destination] = Account{"bob6"}.human(); jv[jss::Amount] = XRP(50).value().getJson(JsonOptions::none); @@ -383,7 +382,6 @@ public: { Json::Value jv; jv[jss::TransactionType] = jss::PaymentChannelCreate; - jv[jss::Flags] = tfUniversal; jv[jss::Account] = Account{"bob6"}.human(); jv[jss::Destination] = Account{"bob7"}.human(); jv[jss::Amount] = XRP(100).value().getJson(JsonOptions::none); diff --git a/src/test/rpc/LedgerEntry_test.cpp b/src/test/rpc/LedgerEntry_test.cpp index cb6f6d45e2..b5cab9d13c 100644 --- a/src/test/rpc/LedgerEntry_test.cpp +++ b/src/test/rpc/LedgerEntry_test.cpp @@ -1259,7 +1259,6 @@ class LedgerEntry_test : public beast::unit_test::suite NetClock::time_point const& cancelAfter) { Json::Value jv; jv[jss::TransactionType] = jss::EscrowCreate; - jv[jss::Flags] = tfUniversal; jv[jss::Account] = account.human(); jv[jss::Destination] = to.human(); jv[jss::Amount] = amount.getJson(JsonOptions::none); @@ -2222,7 +2221,7 @@ class LedgerEntry_test : public beast::unit_test::suite using namespace test::jtx; - Env env(*this, supported_amendments() | featurePermissionedDomains); + Env env(*this, testable_amendments() | featurePermissionedDomains); Account const issuer{"issuer"}; Account const alice{"alice"}; Account const bob{"bob"}; diff --git a/src/test/rpc/LedgerRPC_test.cpp b/src/test/rpc/LedgerRPC_test.cpp index 5b26f43161..9ba9c9a655 100644 --- a/src/test/rpc/LedgerRPC_test.cpp +++ b/src/test/rpc/LedgerRPC_test.cpp @@ -711,6 +711,7 @@ class LedgerRPC_test : public beast::unit_test::suite env.close(); std::string index; + int hashesLedgerEntryIndex = -1; { Json::Value jvParams; jvParams[jss::ledger_index] = 3u; @@ -721,11 +722,27 @@ class LedgerRPC_test : public beast::unit_test::suite env.rpc("json", "ledger", to_string(jvParams))[jss::result]; BEAST_EXPECT(jrr[jss::ledger].isMember(jss::accountState)); BEAST_EXPECT(jrr[jss::ledger][jss::accountState].isArray()); - BEAST_EXPECT(jrr[jss::ledger][jss::accountState].size() == 1u); + + for (auto i = 0; i < jrr[jss::ledger][jss::accountState].size(); + i++) + if (jrr[jss::ledger][jss::accountState][i]["LedgerEntryType"] == + jss::LedgerHashes) + { + index = jrr[jss::ledger][jss::accountState][i]["index"] + .asString(); + hashesLedgerEntryIndex = i; + } + + for (auto const& object : jrr[jss::ledger][jss::accountState]) + if (object["LedgerEntryType"] == jss::LedgerHashes) + index = object["index"].asString(); + + // jss::type is a deprecated field BEAST_EXPECT( - jrr[jss::ledger][jss::accountState][0u]["LedgerEntryType"] == - jss::LedgerHashes); - index = jrr[jss::ledger][jss::accountState][0u]["index"].asString(); + jrr.isMember(jss::warnings) && jrr[jss::warnings].isArray() && + jrr[jss::warnings].size() == 1 && + jrr[jss::warnings][0u][jss::id].asInt() == + warnRPC_FIELDS_DEPRECATED); } { Json::Value jvParams; @@ -737,8 +754,17 @@ class LedgerRPC_test : public beast::unit_test::suite env.rpc("json", "ledger", to_string(jvParams))[jss::result]; BEAST_EXPECT(jrr[jss::ledger].isMember(jss::accountState)); BEAST_EXPECT(jrr[jss::ledger][jss::accountState].isArray()); - BEAST_EXPECT(jrr[jss::ledger][jss::accountState].size() == 1u); - BEAST_EXPECT(jrr[jss::ledger][jss::accountState][0u] == index); + BEAST_EXPECT( + hashesLedgerEntryIndex > 0 && + jrr[jss::ledger][jss::accountState][hashesLedgerEntryIndex] == + index); + + // jss::type is a deprecated field + BEAST_EXPECT( + jrr.isMember(jss::warnings) && jrr[jss::warnings].isArray() && + jrr[jss::warnings].size() == 1 && + jrr[jss::warnings][0u][jss::id].asInt() == + warnRPC_FIELDS_DEPRECATED); } } diff --git a/src/test/rpc/NoRipple_test.cpp b/src/test/rpc/NoRipple_test.cpp index 5c41f25128..926de31e83 100644 --- a/src/test/rpc/NoRipple_test.cpp +++ b/src/test/rpc/NoRipple_test.cpp @@ -293,8 +293,8 @@ public: testPairwise(features); }; using namespace jtx; - auto const sa = supported_amendments(); - withFeatsTests(sa - featureFlowCross); + auto const sa = testable_amendments(); + withFeatsTests(sa - featurePermissionedDEX); withFeatsTests(sa); } }; diff --git a/src/test/rpc/Simulate_test.cpp b/src/test/rpc/Simulate_test.cpp index f27f0c2915..5b3c0d2372 100644 --- a/src/test/rpc/Simulate_test.cpp +++ b/src/test/rpc/Simulate_test.cpp @@ -465,6 +465,36 @@ class Simulate_test : public beast::unit_test::suite } } + void + testInvalidTransactionType() + { + testcase("Invalid transaction type"); + + using namespace jtx; + + Env env(*this); + + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(1000000), alice, bob); + env.close(); + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const seq = env.seq(alice); + auto jt = env.jtnofill( + batch::outer(alice, env.seq(alice), batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(alice, bob, XRP(10)), seq + 1)); + + jt.jv.removeMember(jss::TxnSignature); + Json::Value params; + params[jss::tx_json] = jt.jv; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT(resp[jss::result][jss::error] == "notImpl"); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == "Not implemented."); + } + void testSuccessfulTransaction() { @@ -690,7 +720,11 @@ class Simulate_test : public beast::unit_test::suite Json::Value const& tx) { auto result = resp[jss::result]; checkBasicReturnValidity( - result, tx, env.seq(alice), env.current()->fees().base * 2); + result, + tx, + env.seq(alice), + tx.isMember(jss::Signers) ? env.current()->fees().base * 2 + : env.current()->fees().base); BEAST_EXPECT(result[jss::engine_result] == "tesSUCCESS"); BEAST_EXPECT(result[jss::engine_result_code] == 0); @@ -732,6 +766,10 @@ class Simulate_test : public beast::unit_test::suite tx[jss::Account] = alice.human(); tx[jss::TransactionType] = jss::AccountSet; tx[sfDomain] = newDomain; + + // test with autofill + testTx(env, tx, validateOutput, false); + tx[sfSigners] = Json::arrayValue; { Json::Value signer; @@ -741,7 +779,7 @@ class Simulate_test : public beast::unit_test::suite tx[sfSigners].append(signerOuter); } - // test with autofill + // test with just signer accounts testTx(env, tx, validateOutput, false); tx[sfSigningPubKey] = ""; @@ -750,8 +788,7 @@ class Simulate_test : public beast::unit_test::suite // transaction requires a non-base fee tx[sfFee] = (env.current()->fees().base * 2).jsonClipped().asString(); - tx[sfSigners][0u][sfSigner][jss::SigningPubKey] = - strHex(becky.pk().slice()); + tx[sfSigners][0u][sfSigner][jss::SigningPubKey] = ""; tx[sfSigners][0u][sfSigner][jss::TxnSignature] = ""; // test without autofill @@ -800,11 +837,12 @@ class Simulate_test : public beast::unit_test::suite tx[jss::Account] = env.master.human(); tx[jss::TransactionType] = jss::AccountSet; tx[sfDomain] = newDomain; + // master key is disabled, so this is invalid + tx[jss::SigningPubKey] = strHex(env.master.pk().slice()); // test with autofill testTx(env, tx, testSimulation); - tx[sfSigningPubKey] = ""; tx[sfTxnSignature] = ""; tx[sfSequence] = env.seq(env.master); tx[sfFee] = env.current()->fees().base.jsonClipped().asString(); @@ -814,6 +852,79 @@ class Simulate_test : public beast::unit_test::suite } } + void + testInvalidSingleAndMultiSigningTransaction() + { + testcase( + "Transaction with both single-signing SigningPubKey and " + "multi-signing Signers"); + + using namespace jtx; + Env env(*this); + static auto const newDomain = "123ABC"; + Account const alice("alice"); + Account const becky("becky"); + Account const carol("carol"); + env.fund(XRP(10000), alice); + env.close(); + + // set up valid multisign + env(signers(alice, 1, {{becky, 1}, {carol, 1}})); + env.close(); + + { + std::function const& + testSimulation = [&](Json::Value const& resp, + Json::Value const& tx) { + auto result = resp[jss::result]; + checkBasicReturnValidity( + result, + tx, + env.seq(env.master), + env.current()->fees().base * 2); + + BEAST_EXPECT(result[jss::engine_result] == "temINVALID"); + BEAST_EXPECT(result[jss::engine_result_code] == -277); + BEAST_EXPECT( + result[jss::engine_result_message] == + "The transaction is ill-formed."); + + BEAST_EXPECT( + !result.isMember(jss::meta) && + !result.isMember(jss::meta_blob)); + }; + + Json::Value tx; + + tx[jss::Account] = env.master.human(); + tx[jss::TransactionType] = jss::AccountSet; + tx[sfDomain] = newDomain; + // master key is disabled, so this is invalid + tx[jss::SigningPubKey] = strHex(env.master.pk().slice()); + tx[sfSigners] = Json::arrayValue; + { + Json::Value signer; + signer[jss::Account] = becky.human(); + Json::Value signerOuter; + signerOuter[sfSigner] = signer; + tx[sfSigners].append(signerOuter); + } + + // test with autofill + testTx(env, tx, testSimulation, false); + + tx[sfTxnSignature] = ""; + tx[sfSequence] = env.seq(env.master); + tx[sfFee] = env.current()->fees().base.jsonClipped().asString(); + tx[sfSigners][0u][sfSigner][jss::SigningPubKey] = + strHex(becky.pk().slice()); + tx[sfSigners][0u][sfSigner][jss::TxnSignature] = ""; + + // test without autofill + testTx(env, tx, testSimulation); + } + } + void testMultisignedBadPubKey() { @@ -1081,11 +1192,13 @@ public: { testParamErrors(); testFeeError(); + testInvalidTransactionType(); testSuccessfulTransaction(); testTransactionNonTecFailure(); testTransactionTecFailure(); testSuccessfulTransactionMultisigned(); testTransactionSigningFailure(); + testInvalidSingleAndMultiSigningTransaction(); testMultisignedBadPubKey(); testDeleteExpiredCredentials(); testSuccessfulTransactionNetworkID(); diff --git a/src/test/rpc/Subscribe_test.cpp b/src/test/rpc/Subscribe_test.cpp index 3d1b425422..989afc0acc 100644 --- a/src/test/rpc/Subscribe_test.cpp +++ b/src/test/rpc/Subscribe_test.cpp @@ -1300,11 +1300,284 @@ public: } } + void + testSubBookChanges() + { + testcase("SubBookChanges"); + using namespace jtx; + using namespace std::chrono_literals; + FeatureBitset const all{ + jtx::testable_amendments() | featurePermissionedDomains | + featureCredentials | featurePermissionedDEX}; + + Env env(*this, all); + PermissionedDEX permDex(env); + auto const alice = permDex.alice; + auto const bob = permDex.bob; + auto const carol = permDex.carol; + auto const domainID = permDex.domainID; + auto const gw = permDex.gw; + auto const USD = permDex.USD; + + auto wsc = makeWSClient(env.app().config()); + + Json::Value streams; + streams[jss::streams] = Json::arrayValue; + streams[jss::streams][0u] = "book_changes"; + + auto jv = wsc->invoke("subscribe", streams); + if (!BEAST_EXPECT(jv[jss::status] == "success")) + return; + env(offer(alice, XRP(10), USD(10)), + domain(domainID), + txflags(tfHybrid)); + env.close(); + + env(pay(bob, carol, USD(5)), + path(~USD), + sendmax(XRP(5)), + domain(domainID)); + env.close(); + + BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) { + if (jv[jss::changes].size() != 1) + return false; + + auto const jrOffer = jv[jss::changes][0u]; + return (jv[jss::changes][0u][jss::domain]).asString() == + strHex(domainID) && + jrOffer[jss::currency_a].asString() == "XRP_drops" && + jrOffer[jss::volume_a].asString() == "5000000" && + jrOffer[jss::currency_b].asString() == + "rHUKYAZyUFn8PCZWbPfwHfbVQXTYrYKkHb/USD" && + jrOffer[jss::volume_b].asString() == "5"; + })); + } + + void + testNFToken(FeatureBitset features) + { + // `nftoken_id` is added for `transaction` stream in the `subscribe` + // response for NFTokenMint and NFTokenAcceptOffer. + // + // `nftoken_ids` is added for `transaction` stream in the `subscribe` + // response for NFTokenCancelOffer + // + // `offer_id` is added for `transaction` stream in the `subscribe` + // response for NFTokenCreateOffer + // + // The values of these fields are dependent on the NFTokenID/OfferID + // changed in its corresponding transaction. We want to validate each + // response to make sure the synethic fields hold the right values. + + testcase("Test synthetic fields from Subscribe response"); + + using namespace test::jtx; + using namespace std::chrono_literals; + + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const broker{"broker"}; + + Env env{*this, features}; + env.fund(XRP(10000), alice, bob, broker); + env.close(); + + auto wsc = test::makeWSClient(env.app().config()); + Json::Value stream; + stream[jss::streams] = Json::arrayValue; + stream[jss::streams].append("transactions"); + auto jv = wsc->invoke("subscribe", stream); + + // Verify `nftoken_id` value equals to the NFTokenID that was + // changed in the most recent NFTokenMint or NFTokenAcceptOffer + // transaction + auto verifyNFTokenID = [&](uint256 const& actualNftID) { + BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) { + uint256 nftID; + BEAST_EXPECT( + nftID.parseHex(jv[jss::meta][jss::nftoken_id].asString())); + return nftID == actualNftID; + })); + }; + + // Verify `nftoken_ids` value equals to the NFTokenIDs that were + // changed in the most recent NFTokenCancelOffer transaction + auto verifyNFTokenIDsInCancelOffer = + [&](std::vector actualNftIDs) { + BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) { + std::vector metaIDs; + std::transform( + jv[jss::meta][jss::nftoken_ids].begin(), + jv[jss::meta][jss::nftoken_ids].end(), + std::back_inserter(metaIDs), + [this](Json::Value id) { + uint256 nftID; + BEAST_EXPECT(nftID.parseHex(id.asString())); + return nftID; + }); + // Sort both array to prepare for comparison + std::sort(metaIDs.begin(), metaIDs.end()); + std::sort(actualNftIDs.begin(), actualNftIDs.end()); + + // Make sure the expect number of NFTs is correct + BEAST_EXPECT(metaIDs.size() == actualNftIDs.size()); + + // Check the value of NFT ID in the meta with the + // actual values + for (size_t i = 0; i < metaIDs.size(); ++i) + BEAST_EXPECT(metaIDs[i] == actualNftIDs[i]); + return true; + })); + }; + + // Verify `offer_id` value equals to the offerID that was + // changed in the most recent NFTokenCreateOffer tx + auto verifyNFTokenOfferID = [&](uint256 const& offerID) { + BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) { + uint256 metaOfferID; + BEAST_EXPECT(metaOfferID.parseHex( + jv[jss::meta][jss::offer_id].asString())); + return metaOfferID == offerID; + })); + }; + + // Check new fields in tx meta when for all NFTtransactions + { + // Alice mints 2 NFTs + // Verify the NFTokenIDs are correct in the NFTokenMint tx meta + uint256 const nftId1{ + token::getNextID(env, alice, 0u, tfTransferable)}; + env(token::mint(alice, 0u), txflags(tfTransferable)); + env.close(); + verifyNFTokenID(nftId1); + + uint256 const nftId2{ + token::getNextID(env, alice, 0u, tfTransferable)}; + env(token::mint(alice, 0u), txflags(tfTransferable)); + env.close(); + verifyNFTokenID(nftId2); + + // Alice creates one sell offer for each NFT + // Verify the offer indexes are correct in the NFTokenCreateOffer tx + // meta + uint256 const aliceOfferIndex1 = + keylet::nftoffer(alice, env.seq(alice)).key; + env(token::createOffer(alice, nftId1, drops(1)), + txflags(tfSellNFToken)); + env.close(); + verifyNFTokenOfferID(aliceOfferIndex1); + + uint256 const aliceOfferIndex2 = + keylet::nftoffer(alice, env.seq(alice)).key; + env(token::createOffer(alice, nftId2, drops(1)), + txflags(tfSellNFToken)); + env.close(); + verifyNFTokenOfferID(aliceOfferIndex2); + + // Alice cancels two offers she created + // Verify the NFTokenIDs are correct in the NFTokenCancelOffer tx + // meta + env(token::cancelOffer( + alice, {aliceOfferIndex1, aliceOfferIndex2})); + env.close(); + verifyNFTokenIDsInCancelOffer({nftId1, nftId2}); + + // Bobs creates a buy offer for nftId1 + // Verify the offer id is correct in the NFTokenCreateOffer tx meta + auto const bobBuyOfferIndex = + keylet::nftoffer(bob, env.seq(bob)).key; + env(token::createOffer(bob, nftId1, drops(1)), token::owner(alice)); + env.close(); + verifyNFTokenOfferID(bobBuyOfferIndex); + + // Alice accepts bob's buy offer + // Verify the NFTokenID is correct in the NFTokenAcceptOffer tx meta + env(token::acceptBuyOffer(alice, bobBuyOfferIndex)); + env.close(); + verifyNFTokenID(nftId1); + } + + // Check `nftoken_ids` in brokered mode + { + // Alice mints a NFT + uint256 const nftId{ + token::getNextID(env, alice, 0u, tfTransferable)}; + env(token::mint(alice, 0u), txflags(tfTransferable)); + env.close(); + verifyNFTokenID(nftId); + + // Alice creates sell offer and set broker as destination + uint256 const offerAliceToBroker = + keylet::nftoffer(alice, env.seq(alice)).key; + env(token::createOffer(alice, nftId, drops(1)), + token::destination(broker), + txflags(tfSellNFToken)); + env.close(); + verifyNFTokenOfferID(offerAliceToBroker); + + // Bob creates buy offer + uint256 const offerBobToBroker = + keylet::nftoffer(bob, env.seq(bob)).key; + env(token::createOffer(bob, nftId, drops(1)), token::owner(alice)); + env.close(); + verifyNFTokenOfferID(offerBobToBroker); + + // Check NFTokenID meta for NFTokenAcceptOffer in brokered mode + env(token::brokerOffers( + broker, offerBobToBroker, offerAliceToBroker)); + env.close(); + verifyNFTokenID(nftId); + } + + // Check if there are no duplicate nft id in Cancel transactions where + // multiple offers are cancelled for the same NFT + { + // Alice mints a NFT + uint256 const nftId{ + token::getNextID(env, alice, 0u, tfTransferable)}; + env(token::mint(alice, 0u), txflags(tfTransferable)); + env.close(); + verifyNFTokenID(nftId); + + // Alice creates 2 sell offers for the same NFT + uint256 const aliceOfferIndex1 = + keylet::nftoffer(alice, env.seq(alice)).key; + env(token::createOffer(alice, nftId, drops(1)), + txflags(tfSellNFToken)); + env.close(); + verifyNFTokenOfferID(aliceOfferIndex1); + + uint256 const aliceOfferIndex2 = + keylet::nftoffer(alice, env.seq(alice)).key; + env(token::createOffer(alice, nftId, drops(1)), + txflags(tfSellNFToken)); + env.close(); + verifyNFTokenOfferID(aliceOfferIndex2); + + // Make sure the metadata only has 1 nft id, since both offers are + // for the same nft + env(token::cancelOffer( + alice, {aliceOfferIndex1, aliceOfferIndex2})); + env.close(); + verifyNFTokenIDsInCancelOffer({nftId}); + } + + if (features[featureNFTokenMintOffer]) + { + uint256 const aliceMintWithOfferIndex1 = + keylet::nftoffer(alice, env.seq(alice)).key; + env(token::mint(alice), token::amount(XRP(0))); + env.close(); + verifyNFTokenOfferID(aliceMintWithOfferIndex1); + } + } + void run() override { using namespace test::jtx; - FeatureBitset const all{supported_amendments()}; + FeatureBitset const all{testable_amendments()}; FeatureBitset const xrpFees{featureXRPFees}; testServer(); @@ -1318,6 +1591,9 @@ public: testSubErrors(false); testSubByUrl(); testHistoryTxStream(); + testSubBookChanges(); + testNFToken(all); + testNFToken(all - featureNFTokenMintOffer); } }; diff --git a/src/test/rpc/Transaction_test.cpp b/src/test/rpc/Transaction_test.cpp index 724a3a0517..e1db485572 100644 --- a/src/test/rpc/Transaction_test.cpp +++ b/src/test/rpc/Transaction_test.cpp @@ -941,7 +941,7 @@ public: forAllApiVersions( std::bind_front(&Transaction_test::testBinaryRequest, this)); - FeatureBitset const all{supported_amendments()}; + FeatureBitset const all{testable_amendments()}; testWithFeats(all); } diff --git a/src/test/unit_test/SuiteJournal.h b/src/test/unit_test/SuiteJournal.h index b5c59f3d29..d56c297b0a 100644 --- a/src/test/unit_test/SuiteJournal.h +++ b/src/test/unit_test/SuiteJournal.h @@ -94,6 +94,8 @@ SuiteJournalSink::writeAlways( return "FTL:"; }(); + static std::mutex log_mutex; + std::lock_guard lock(log_mutex); suite_.log << s << partition_ << text << std::endl; } diff --git a/src/tests/README.md b/src/tests/README.md new file mode 100644 index 0000000000..8065316580 --- /dev/null +++ b/src/tests/README.md @@ -0,0 +1,4 @@ +# Unit tests +This directory contains unit tests for the project. The difference from existing `src/test` folder +is that we switch to 3rd party testing framework (doctest). We intend to gradually move existing tests +from our own framework to doctest and such tests will be moved to this new folder. diff --git a/src/tests/libxrpl/CMakeLists.txt b/src/tests/libxrpl/CMakeLists.txt new file mode 100644 index 0000000000..68c6fa6cb3 --- /dev/null +++ b/src/tests/libxrpl/CMakeLists.txt @@ -0,0 +1,14 @@ +include(xrpl_add_test) + +# Test requirements. +find_package(doctest REQUIRED) + +# Common library dependencies for the rest of the tests. +add_library(xrpl.imports.test INTERFACE) +target_link_libraries(xrpl.imports.test INTERFACE doctest::doctest xrpl.libxrpl) + +# One test for each module. +xrpl_add_test(basics) +target_link_libraries(xrpl.test.basics PRIVATE xrpl.imports.test) +xrpl_add_test(crypto) +target_link_libraries(xrpl.test.crypto PRIVATE xrpl.imports.test) diff --git a/src/tests/libxrpl/basics/RangeSet.cpp b/src/tests/libxrpl/basics/RangeSet.cpp new file mode 100644 index 0000000000..ac0e1d9551 --- /dev/null +++ b/src/tests/libxrpl/basics/RangeSet.cpp @@ -0,0 +1,129 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +#include + +#include +#include + +using namespace ripple; + +TEST_SUITE_BEGIN("RangeSet"); + +TEST_CASE("prevMissing") +{ + // Set will include: + // [ 0, 5] + // [10,15] + // [20,25] + // etc... + + RangeSet set; + for (std::uint32_t i = 0; i < 10; ++i) + set.insert(range(10 * i, 10 * i + 5)); + + for (std::uint32_t i = 1; i < 100; ++i) + { + std::optional expected; + // no prev missing in domain for i <= 6 + if (i > 6) + { + std::uint32_t const oneBelowRange = (10 * (i / 10)) - 1; + + expected = ((i % 10) > 6) ? (i - 1) : oneBelowRange; + } + CHECK(prevMissing(set, i) == expected); + } +} + +TEST_CASE("toString") +{ + RangeSet set; + CHECK(to_string(set) == "empty"); + + set.insert(1); + CHECK(to_string(set) == "1"); + + set.insert(range(4u, 6u)); + CHECK(to_string(set) == "1,4-6"); + + set.insert(2); + CHECK(to_string(set) == "1-2,4-6"); + + set.erase(range(4u, 5u)); + CHECK(to_string(set) == "1-2,6"); +} + +TEST_CASE("fromString") +{ + RangeSet set; + + CHECK(!from_string(set, "")); + CHECK(boost::icl::length(set) == 0); + + CHECK(!from_string(set, "#")); + CHECK(boost::icl::length(set) == 0); + + CHECK(!from_string(set, ",")); + CHECK(boost::icl::length(set) == 0); + + CHECK(!from_string(set, ",-")); + CHECK(boost::icl::length(set) == 0); + + CHECK(!from_string(set, "1,,2")); + CHECK(boost::icl::length(set) == 0); + + CHECK(from_string(set, "1")); + CHECK(boost::icl::length(set) == 1); + CHECK(boost::icl::first(set) == 1); + + CHECK(from_string(set, "1,1")); + CHECK(boost::icl::length(set) == 1); + CHECK(boost::icl::first(set) == 1); + + CHECK(from_string(set, "1-1")); + CHECK(boost::icl::length(set) == 1); + CHECK(boost::icl::first(set) == 1); + + CHECK(from_string(set, "1,4-6")); + CHECK(boost::icl::length(set) == 4); + CHECK(boost::icl::first(set) == 1); + CHECK(!boost::icl::contains(set, 2)); + CHECK(!boost::icl::contains(set, 3)); + CHECK(boost::icl::contains(set, 4)); + CHECK(boost::icl::contains(set, 5)); + CHECK(boost::icl::last(set) == 6); + + CHECK(from_string(set, "1-2,4-6")); + CHECK(boost::icl::length(set) == 5); + CHECK(boost::icl::first(set) == 1); + CHECK(boost::icl::contains(set, 2)); + CHECK(boost::icl::contains(set, 4)); + CHECK(boost::icl::last(set) == 6); + + CHECK(from_string(set, "1-2,6")); + CHECK(boost::icl::length(set) == 3); + CHECK(boost::icl::first(set) == 1); + CHECK(boost::icl::contains(set, 2)); + CHECK(boost::icl::last(set) == 6); +} + +TEST_SUITE_END(); diff --git a/src/tests/libxrpl/basics/Slice.cpp b/src/tests/libxrpl/basics/Slice.cpp new file mode 100644 index 0000000000..eabd9b7dc7 --- /dev/null +++ b/src/tests/libxrpl/basics/Slice.cpp @@ -0,0 +1,105 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +#include + +#include +#include + +using namespace ripple; + +static std::uint8_t const data[] = { + 0xa8, 0xa1, 0x38, 0x45, 0x23, 0xec, 0xe4, 0x23, 0x71, 0x6d, 0x2a, + 0x18, 0xb4, 0x70, 0xcb, 0xf5, 0xac, 0x2d, 0x89, 0x4d, 0x19, 0x9c, + 0xf0, 0x2c, 0x15, 0xd1, 0xf9, 0x9b, 0x66, 0xd2, 0x30, 0xd3}; + +TEST_SUITE_BEGIN("Slice"); + +TEST_CASE("equality & inequality") +{ + Slice const s0{}; + + CHECK(s0.size() == 0); + CHECK(s0.data() == nullptr); + CHECK(s0 == s0); + + // Test slices of equal and unequal size pointing to same data: + for (std::size_t i = 0; i != sizeof(data); ++i) + { + Slice const s1{data, i}; + + CHECK(s1.size() == i); + CHECK(s1.data() != nullptr); + + if (i == 0) + CHECK(s1 == s0); + else + CHECK(s1 != s0); + + for (std::size_t j = 0; j != sizeof(data); ++j) + { + Slice const s2{data, j}; + + if (i == j) + CHECK(s1 == s2); + else + CHECK(s1 != s2); + } + } + + // Test slices of equal size but pointing to different data: + std::array a; + std::array b; + + for (std::size_t i = 0; i != sizeof(data); ++i) + a[i] = b[i] = data[i]; + + CHECK(makeSlice(a) == makeSlice(b)); + b[7]++; + CHECK(makeSlice(a) != makeSlice(b)); + a[7]++; + CHECK(makeSlice(a) == makeSlice(b)); +} + +TEST_CASE("indexing") +{ + Slice const s{data, sizeof(data)}; + + for (std::size_t i = 0; i != sizeof(data); ++i) + CHECK(s[i] == data[i]); +} + +TEST_CASE("advancing") +{ + for (std::size_t i = 0; i < sizeof(data); ++i) + { + for (std::size_t j = 0; i + j < sizeof(data); ++j) + { + Slice s(data + i, sizeof(data) - i); + s += j; + + CHECK(s.data() == data + i + j); + CHECK(s.size() == sizeof(data) - i - j); + } + } +} + +TEST_SUITE_END(); diff --git a/src/tests/libxrpl/basics/base64.cpp b/src/tests/libxrpl/basics/base64.cpp new file mode 100644 index 0000000000..fe9b86abb1 --- /dev/null +++ b/src/tests/libxrpl/basics/base64.cpp @@ -0,0 +1,67 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +#include + +#include + +using namespace ripple; + +static void +check(std::string const& in, std::string const& out) +{ + auto const encoded = base64_encode(in); + CHECK(encoded == out); + CHECK(base64_decode(encoded) == in); +} + +TEST_CASE("base64") +{ + check("", ""); + check("f", "Zg=="); + check("fo", "Zm8="); + check("foo", "Zm9v"); + check("foob", "Zm9vYg=="); + check("fooba", "Zm9vYmE="); + check("foobar", "Zm9vYmFy"); + + check( + "Man is distinguished, not only by his reason, but by this " + "singular passion from " + "other animals, which is a lust of the mind, that by a " + "perseverance of delight " + "in the continued and indefatigable generation of knowledge, " + "exceeds the short " + "vehemence of any carnal pleasure.", + "TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dC" + "BieSB0aGlz" + "IHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYWxzLCB3aGljaCBpcyBhIG" + "x1c3Qgb2Yg" + "dGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aG" + "UgY29udGlu" + "dWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleG" + "NlZWRzIHRo" + "ZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4="); + + std::string const notBase64 = "not_base64!!"; + std::string const truncated = "not"; + CHECK(base64_decode(notBase64) == base64_decode(truncated)); +} diff --git a/src/test/basics/contract_test.cpp b/src/tests/libxrpl/basics/contract.cpp similarity index 60% rename from src/test/basics/contract_test.cpp rename to src/tests/libxrpl/basics/contract.cpp index 9595dbabcc..9ddf044f17 100644 --- a/src/test/basics/contract_test.cpp +++ b/src/tests/libxrpl/basics/contract.cpp @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ /* This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2012, 2013 Ripple Labs Inc. + Copyright (c) 2012 Ripple Labs Inc. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above @@ -18,46 +18,39 @@ //============================================================================== #include -#include +#include + +#include #include -namespace ripple { +using namespace ripple; -class contract_test : public beast::unit_test::suite +TEST_CASE("contract") { -public: - void - run() override + try { + Throw("Throw test"); + } + catch (std::runtime_error const& e1) + { + CHECK(std::string(e1.what()) == "Throw test"); + try { - Throw("Throw test"); + Rethrow(); } - catch (std::runtime_error const& e1) + catch (std::runtime_error const& e2) { - BEAST_EXPECT(std::string(e1.what()) == "Throw test"); - - try - { - Rethrow(); - } - catch (std::runtime_error const& e2) - { - BEAST_EXPECT(std::string(e2.what()) == "Throw test"); - } - catch (...) - { - BEAST_EXPECT(false); - } + CHECK(std::string(e2.what()) == "Throw test"); } catch (...) { - BEAST_EXPECT(false); + CHECK(false); } } -}; - -BEAST_DEFINE_TESTSUITE(contract, basics, ripple); - -} // namespace ripple + catch (...) + { + CHECK(false); + } +} diff --git a/src/tests/libxrpl/basics/main.cpp b/src/tests/libxrpl/basics/main.cpp new file mode 100644 index 0000000000..0a3f254ea8 --- /dev/null +++ b/src/tests/libxrpl/basics/main.cpp @@ -0,0 +1,2 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include diff --git a/src/tests/libxrpl/basics/mulDiv.cpp b/src/tests/libxrpl/basics/mulDiv.cpp new file mode 100644 index 0000000000..bdbbfdc741 --- /dev/null +++ b/src/tests/libxrpl/basics/mulDiv.cpp @@ -0,0 +1,64 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +#include + +#include +#include + +using namespace ripple; + +TEST_CASE("mulDiv") +{ + auto const max = std::numeric_limits::max(); + std::uint64_t const max32 = std::numeric_limits::max(); + + auto result = mulDiv(85, 20, 5); + REQUIRE(result); + CHECK(*result == 340); + result = mulDiv(20, 85, 5); + REQUIRE(result); + CHECK(*result == 340); + + result = mulDiv(0, max - 1, max - 3); + REQUIRE(result); + CHECK(*result == 0); + result = mulDiv(max - 1, 0, max - 3); + REQUIRE(result); + CHECK(*result == 0); + + result = mulDiv(max, 2, max / 2); + REQUIRE(result); + CHECK(*result == 4); + result = mulDiv(max, 1000, max / 1000); + REQUIRE(result); + CHECK(*result == 1000000); + result = mulDiv(max, 1000, max / 1001); + REQUIRE(result); + CHECK(*result == 1001000); + result = mulDiv(max32 + 1, max32 + 1, 5); + REQUIRE(result); + CHECK(*result == 3689348814741910323); + + // Overflow + result = mulDiv(max - 1, max - 2, 5); + CHECK(!result); +} diff --git a/src/tests/libxrpl/basics/scope.cpp b/src/tests/libxrpl/basics/scope.cpp new file mode 100644 index 0000000000..c9cfc1e7f8 --- /dev/null +++ b/src/tests/libxrpl/basics/scope.cpp @@ -0,0 +1,174 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2021 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +#include + +using namespace ripple; + +TEST_CASE("scope_exit") +{ + // scope_exit always executes the functor on destruction, + // unless release() is called + int i = 0; + { + scope_exit x{[&i]() { i = 1; }}; + } + CHECK(i == 1); + { + scope_exit x{[&i]() { i = 2; }}; + x.release(); + } + CHECK(i == 1); + { + scope_exit x{[&i]() { i += 2; }}; + auto x2 = std::move(x); + } + CHECK(i == 3); + { + scope_exit x{[&i]() { i = 4; }}; + x.release(); + auto x2 = std::move(x); + } + CHECK(i == 3); + { + try + { + scope_exit x{[&i]() { i = 5; }}; + throw 1; + } + catch (...) + { + } + } + CHECK(i == 5); + { + try + { + scope_exit x{[&i]() { i = 6; }}; + x.release(); + throw 1; + } + catch (...) + { + } + } + CHECK(i == 5); +} + +TEST_CASE("scope_fail") +{ + // scope_fail executes the functor on destruction only + // if an exception is unwinding, unless release() is called + int i = 0; + { + scope_fail x{[&i]() { i = 1; }}; + } + CHECK(i == 0); + { + scope_fail x{[&i]() { i = 2; }}; + x.release(); + } + CHECK(i == 0); + { + scope_fail x{[&i]() { i = 3; }}; + auto x2 = std::move(x); + } + CHECK(i == 0); + { + scope_fail x{[&i]() { i = 4; }}; + x.release(); + auto x2 = std::move(x); + } + CHECK(i == 0); + { + try + { + scope_fail x{[&i]() { i = 5; }}; + throw 1; + } + catch (...) + { + } + } + CHECK(i == 5); + { + try + { + scope_fail x{[&i]() { i = 6; }}; + x.release(); + throw 1; + } + catch (...) + { + } + } + CHECK(i == 5); +} + +TEST_CASE("scope_success") +{ + // scope_success executes the functor on destruction only + // if an exception is not unwinding, unless release() is called + int i = 0; + { + scope_success x{[&i]() { i = 1; }}; + } + CHECK(i == 1); + { + scope_success x{[&i]() { i = 2; }}; + x.release(); + } + CHECK(i == 1); + { + scope_success x{[&i]() { i += 2; }}; + auto x2 = std::move(x); + } + CHECK(i == 3); + { + scope_success x{[&i]() { i = 4; }}; + x.release(); + auto x2 = std::move(x); + } + CHECK(i == 3); + { + try + { + scope_success x{[&i]() { i = 5; }}; + throw 1; + } + catch (...) + { + } + } + CHECK(i == 3); + { + try + { + scope_success x{[&i]() { i = 6; }}; + x.release(); + throw 1; + } + catch (...) + { + } + } + CHECK(i == 3); +} diff --git a/src/tests/libxrpl/basics/tagged_integer.cpp b/src/tests/libxrpl/basics/tagged_integer.cpp new file mode 100644 index 0000000000..d699b64a70 --- /dev/null +++ b/src/tests/libxrpl/basics/tagged_integer.cpp @@ -0,0 +1,247 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2014 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +#include + +#include + +using namespace ripple; + +struct Tag1 +{ +}; +struct Tag2 +{ +}; + +// Static checks that types are not interoperable + +using TagUInt1 = tagged_integer; +using TagUInt2 = tagged_integer; +using TagUInt3 = tagged_integer; + +// Check construction of tagged_integers +static_assert( + std::is_constructible::value, + "TagUInt1 should be constructible using a std::uint32_t"); + +static_assert( + !std::is_constructible::value, + "TagUInt1 should not be constructible using a std::uint64_t"); + +static_assert( + std::is_constructible::value, + "TagUInt3 should be constructible using a std::uint32_t"); + +static_assert( + std::is_constructible::value, + "TagUInt3 should be constructible using a std::uint64_t"); + +// Check assignment of tagged_integers +static_assert( + !std::is_assignable::value, + "TagUInt1 should not be assignable with a std::uint32_t"); + +static_assert( + !std::is_assignable::value, + "TagUInt1 should not be assignable with a std::uint64_t"); + +static_assert( + !std::is_assignable::value, + "TagUInt3 should not be assignable with a std::uint32_t"); + +static_assert( + !std::is_assignable::value, + "TagUInt3 should not be assignable with a std::uint64_t"); + +static_assert( + std::is_assignable::value, + "TagUInt1 should be assignable with a TagUInt1"); + +static_assert( + !std::is_assignable::value, + "TagUInt1 should not be assignable with a TagUInt2"); + +static_assert( + std::is_assignable::value, + "TagUInt3 should be assignable with a TagUInt1"); + +static_assert( + !std::is_assignable::value, + "TagUInt1 should not be assignable with a TagUInt3"); + +static_assert( + !std::is_assignable::value, + "TagUInt3 should not be assignable with a TagUInt1"); + +// Check convertibility of tagged_integers +static_assert( + !std::is_convertible::value, + "std::uint32_t should not be convertible to a TagUInt1"); + +static_assert( + !std::is_convertible::value, + "std::uint32_t should not be convertible to a TagUInt3"); + +static_assert( + !std::is_convertible::value, + "std::uint64_t should not be convertible to a TagUInt3"); + +static_assert( + !std::is_convertible::value, + "std::uint64_t should not be convertible to a TagUInt2"); + +static_assert( + !std::is_convertible::value, + "TagUInt1 should not be convertible to TagUInt2"); + +static_assert( + !std::is_convertible::value, + "TagUInt1 should not be convertible to TagUInt3"); + +static_assert( + !std::is_convertible::value, + "TagUInt2 should not be convertible to a TagUInt3"); + +TEST_SUITE_BEGIN("tagged_integer"); + +using TagInt = tagged_integer; + +TEST_CASE("comparison operators") +{ + TagInt const zero(0); + TagInt const one(1); + + CHECK(one == one); + CHECK(!(one == zero)); + + CHECK(one != zero); + CHECK(!(one != one)); + + CHECK(zero < one); + CHECK(!(one < zero)); + + CHECK(one > zero); + CHECK(!(zero > one)); + + CHECK(one >= one); + CHECK(one >= zero); + CHECK(!(zero >= one)); + + CHECK(zero <= one); + CHECK(zero <= zero); + CHECK(!(one <= zero)); +} + +TEST_CASE("increment / decrement operators") +{ + TagInt const zero(0); + TagInt const one(1); + TagInt a{0}; + ++a; + CHECK(a == one); + --a; + CHECK(a == zero); + a++; + CHECK(a == one); + a--; + CHECK(a == zero); +} + +TEST_CASE("arithmetic operators") +{ + TagInt a{-2}; + CHECK(+a == TagInt{-2}); + CHECK(-a == TagInt{2}); + CHECK(TagInt{-3} + TagInt{4} == TagInt{1}); + CHECK(TagInt{-3} - TagInt{4} == TagInt{-7}); + CHECK(TagInt{-3} * TagInt{4} == TagInt{-12}); + CHECK(TagInt{8} / TagInt{4} == TagInt{2}); + CHECK(TagInt{7} % TagInt{4} == TagInt{3}); + + CHECK(~TagInt{8} == TagInt{~TagInt::value_type{8}}); + CHECK((TagInt{6} & TagInt{3}) == TagInt{2}); + CHECK((TagInt{6} | TagInt{3}) == TagInt{7}); + CHECK((TagInt{6} ^ TagInt{3}) == TagInt{5}); + + CHECK((TagInt{4} << TagInt{2}) == TagInt{16}); + CHECK((TagInt{16} >> TagInt{2}) == TagInt{4}); +} + +TEST_CASE("assignment operators") +{ + TagInt a{-2}; + TagInt b{0}; + b = a; + CHECK(b == TagInt{-2}); + + // -3 + 4 == 1 + a = TagInt{-3}; + a += TagInt{4}; + CHECK(a == TagInt{1}); + + // -3 - 4 == -7 + a = TagInt{-3}; + a -= TagInt{4}; + CHECK(a == TagInt{-7}); + + // -3 * 4 == -12 + a = TagInt{-3}; + a *= TagInt{4}; + CHECK(a == TagInt{-12}); + + // 8/4 == 2 + a = TagInt{8}; + a /= TagInt{4}; + CHECK(a == TagInt{2}); + + // 7 % 4 == 3 + a = TagInt{7}; + a %= TagInt{4}; + CHECK(a == TagInt{3}); + + // 6 & 3 == 2 + a = TagInt{6}; + a /= TagInt{3}; + CHECK(a == TagInt{2}); + + // 6 | 3 == 7 + a = TagInt{6}; + a |= TagInt{3}; + CHECK(a == TagInt{7}); + + // 6 ^ 3 == 5 + a = TagInt{6}; + a ^= TagInt{3}; + CHECK(a == TagInt{5}); + + // 4 << 2 == 16 + a = TagInt{4}; + a <<= TagInt{2}; + CHECK(a == TagInt{16}); + + // 16 >> 2 == 4 + a = TagInt{16}; + a >>= TagInt{2}; + CHECK(a == TagInt{4}); +} + +TEST_SUITE_END(); diff --git a/src/tests/libxrpl/crypto/csprng.cpp b/src/tests/libxrpl/crypto/csprng.cpp new file mode 100644 index 0000000000..a55d49b67c --- /dev/null +++ b/src/tests/libxrpl/crypto/csprng.cpp @@ -0,0 +1,34 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012-2017 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +#include + +using namespace ripple; + +TEST_CASE("get values") +{ + auto& engine = crypto_prng(); + auto rand_val = engine(); + CHECK(rand_val >= engine.min()); + CHECK(rand_val <= engine.max()); + uint16_t twoByte{0}; + engine(&twoByte, sizeof(uint16_t)); +} diff --git a/src/tests/libxrpl/crypto/main.cpp b/src/tests/libxrpl/crypto/main.cpp new file mode 100644 index 0000000000..0a3f254ea8 --- /dev/null +++ b/src/tests/libxrpl/crypto/main.cpp @@ -0,0 +1,2 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include diff --git a/src/xrpld/app/ledger/LedgerToJson.h b/src/xrpld/app/ledger/LedgerToJson.h index 40be57fc9c..be017bca86 100644 --- a/src/xrpld/app/ledger/LedgerToJson.h +++ b/src/xrpld/app/ledger/LedgerToJson.h @@ -37,9 +37,8 @@ struct LedgerFill ReadView const& l, RPC::Context* ctx, int o = 0, - std::vector q = {}, - LedgerEntryType t = ltANY) - : ledger(l), options(o), txQueue(std::move(q)), type(t), context(ctx) + std::vector q = {}) + : ledger(l), options(o), txQueue(std::move(q)), context(ctx) { if (context) closeTime = context->ledgerMaster.getCloseTimeBySeq(ledger.seq()); @@ -58,7 +57,6 @@ struct LedgerFill ReadView const& ledger; int options; std::vector txQueue; - LedgerEntryType type; RPC::Context* context; std::optional closeTime; }; diff --git a/src/xrpld/app/ledger/OrderBookDB.cpp b/src/xrpld/app/ledger/OrderBookDB.cpp index b8a7b54008..433a993772 100644 --- a/src/xrpld/app/ledger/OrderBookDB.cpp +++ b/src/xrpld/app/ledger/OrderBookDB.cpp @@ -89,6 +89,8 @@ OrderBookDB::update(std::shared_ptr const& ledger) decltype(allBooks_) allBooks; decltype(xrpBooks_) xrpBooks; + decltype(domainBooks_) domainBooks; + decltype(xrpDomainBooks_) xrpDomainBooks; allBooks.reserve(allBooks_.size()); xrpBooks.reserve(xrpBooks_.size()); @@ -120,10 +122,16 @@ OrderBookDB::update(std::shared_ptr const& ledger) book.in.account = sle->getFieldH160(sfTakerPaysIssuer); book.out.currency = sle->getFieldH160(sfTakerGetsCurrency); book.out.account = sle->getFieldH160(sfTakerGetsIssuer); + book.domain = (*sle)[~sfDomainID]; - allBooks[book.in].insert(book.out); + if (book.domain) + domainBooks_[{book.in, *book.domain}].insert(book.out); + else + allBooks[book.in].insert(book.out); - if (isXRP(book.out)) + if (book.domain && isXRP(book.out)) + xrpDomainBooks.insert({book.in, *book.domain}); + else if (isXRP(book.out)) xrpBooks.insert(book.in); ++cnt; @@ -160,6 +168,8 @@ OrderBookDB::update(std::shared_ptr const& ledger) std::lock_guard sl(mLock); allBooks_.swap(allBooks); xrpBooks_.swap(xrpBooks); + domainBooks_.swap(domainBooks); + xrpDomainBooks_.swap(xrpDomainBooks); } app_.getLedgerMaster().newOrderBookDB(); @@ -172,47 +182,77 @@ OrderBookDB::addOrderBook(Book const& book) std::lock_guard sl(mLock); - allBooks_[book.in].insert(book.out); + if (book.domain) + domainBooks_[{book.in, *book.domain}].insert(book.out); + else + allBooks_[book.in].insert(book.out); - if (toXRP) + if (book.domain && toXRP) + xrpDomainBooks_.insert({book.in, *book.domain}); + else if (toXRP) xrpBooks_.insert(book.in); } // return list of all orderbooks that want this issuerID and currencyID std::vector -OrderBookDB::getBooksByTakerPays(Issue const& issue) +OrderBookDB::getBooksByTakerPays( + Issue const& issue, + std::optional const& domain) { std::vector ret; { std::lock_guard sl(mLock); - if (auto it = allBooks_.find(issue); it != allBooks_.end()) - { - ret.reserve(it->second.size()); + auto getBooks = [&](auto const& container, auto const& key) { + if (auto it = container.find(key); it != container.end()) + { + auto const& books = it->second; + ret.reserve(books.size()); - for (auto const& gets : it->second) - ret.push_back(Book(issue, gets)); - } + for (auto const& gets : books) + ret.emplace_back(issue, gets, domain); + } + }; + + if (!domain) + getBooks(allBooks_, issue); + else + getBooks(domainBooks_, std::make_pair(issue, *domain)); } return ret; } int -OrderBookDB::getBookSize(Issue const& issue) +OrderBookDB::getBookSize( + Issue const& issue, + std::optional const& domain) { std::lock_guard sl(mLock); - if (auto it = allBooks_.find(issue); it != allBooks_.end()) - return static_cast(it->second.size()); + + if (!domain) + { + if (auto it = allBooks_.find(issue); it != allBooks_.end()) + return static_cast(it->second.size()); + } + else + { + if (auto it = domainBooks_.find({issue, *domain}); + it != domainBooks_.end()) + return static_cast(it->second.size()); + } + return 0; } bool -OrderBookDB::isBookToXRP(Issue const& issue) +OrderBookDB::isBookToXRP(Issue const& issue, std::optional domain) { std::lock_guard sl(mLock); - return xrpBooks_.count(issue) > 0; + if (domain) + return xrpDomainBooks_.contains({issue, *domain}); + return xrpBooks_.contains(issue); } BookListeners::pointer @@ -228,7 +268,8 @@ OrderBookDB::makeBookListeners(Book const& book) mListeners[book] = ret; XRPL_ASSERT( getBookListeners(book) == ret, - "ripple::OrderBookDB::makeBookListeners : result roundtrip lookup"); + "ripple::OrderBookDB::makeBookListeners : result roundtrip " + "lookup"); } return ret; @@ -278,7 +319,8 @@ OrderBookDB::processTxn( { auto listeners = getBookListeners( {data->getFieldAmount(sfTakerGets).issue(), - data->getFieldAmount(sfTakerPays).issue()}); + data->getFieldAmount(sfTakerPays).issue(), + (*data)[~sfDomainID]}); if (listeners) listeners->publish(jvObj, havePublished); } diff --git a/src/xrpld/app/ledger/OrderBookDB.h b/src/xrpld/app/ledger/OrderBookDB.h index bc36f8a301..89c20b7074 100644 --- a/src/xrpld/app/ledger/OrderBookDB.h +++ b/src/xrpld/app/ledger/OrderBookDB.h @@ -25,8 +25,10 @@ #include #include +#include #include +#include namespace ripple { @@ -46,15 +48,19 @@ public: /** @return a list of all orderbooks that want this issuerID and currencyID. */ std::vector - getBooksByTakerPays(Issue const&); + getBooksByTakerPays( + Issue const&, + std::optional const& domain = std::nullopt); /** @return a count of all orderbooks that want this issuerID and currencyID. */ int - getBookSize(Issue const&); + getBookSize( + Issue const&, + std::optional const& domain = std::nullopt); bool - isBookToXRP(Issue const&); + isBookToXRP(Issue const&, std::optional domain = std::nullopt); BookListeners::pointer getBookListeners(Book const&); @@ -74,9 +80,15 @@ private: // Maps order books by "issue in" to "issue out": hardened_hash_map> allBooks_; + hardened_hash_map, hardened_hash_set> + domainBooks_; + // does an order book to XRP exist hash_set xrpBooks_; + // does an order book to XRP exist + hash_set> xrpDomainBooks_; + std::recursive_mutex mLock; using BookToListenersMap = hash_map; diff --git a/src/xrpld/app/ledger/detail/BuildLedger.cpp b/src/xrpld/app/ledger/detail/BuildLedger.cpp index 954507a006..4305426753 100644 --- a/src/xrpld/app/ledger/detail/BuildLedger.cpp +++ b/src/xrpld/app/ledger/detail/BuildLedger.cpp @@ -208,11 +208,17 @@ buildLedger( applyTransactions(app, built, txns, failedTxns, accum, j); if (!txns.empty() || !failedTxns.empty()) - JLOG(j.debug()) << "Applied " << applied << " transactions; " - << failedTxns.size() << " failed and " - << txns.size() << " will be retried."; + JLOG(j.debug()) + << "Applied " << applied << " transactions; " + << failedTxns.size() << " failed and " << txns.size() + << " will be retried. " + << "Total transactions in ledger (including Inner Batch): " + << accum.txCount(); else - JLOG(j.debug()) << "Applied " << applied << " transactions."; + JLOG(j.debug()) + << "Applied " << applied << " transactions. " + << "Total transactions in ledger (including Inner Batch): " + << accum.txCount(); }); } diff --git a/src/xrpld/app/ledger/detail/InboundLedger.cpp b/src/xrpld/app/ledger/detail/InboundLedger.cpp index c1eed3a9f3..eafa939506 100644 --- a/src/xrpld/app/ledger/detail/InboundLedger.cpp +++ b/src/xrpld/app/ledger/detail/InboundLedger.cpp @@ -502,15 +502,17 @@ InboundLedger::trigger(std::shared_ptr const& peer, TriggerReason reason) if (auto stream = journal_.debug()) { - stream << "Trigger acquiring ledger " << hash_; + std::stringstream ss; + ss << "Trigger acquiring ledger " << hash_; if (peer) - stream << " from " << peer; + ss << " from " << peer; if (complete_ || failed_) - stream << "complete=" << complete_ << " failed=" << failed_; + ss << " complete=" << complete_ << " failed=" << failed_; else - stream << "header=" << mHaveHeader << " tx=" << mHaveTransactions - << " as=" << mHaveState; + ss << " header=" << mHaveHeader << " tx=" << mHaveTransactions + << " as=" << mHaveState; + stream << ss.str(); } if (!mHaveHeader) diff --git a/src/xrpld/app/ledger/detail/LedgerToJson.cpp b/src/xrpld/app/ledger/detail/LedgerToJson.cpp index 3e4f4b8f0a..0e6f81dfbc 100644 --- a/src/xrpld/app/ledger/detail/LedgerToJson.cpp +++ b/src/xrpld/app/ledger/detail/LedgerToJson.cpp @@ -268,19 +268,16 @@ fillJsonState(Object& json, LedgerFill const& fill) for (auto const& sle : ledger.sles) { - if (fill.type == ltANY || sle->getType() == fill.type) + if (binary) { - if (binary) - { - auto&& obj = appendObject(array); - obj[jss::hash] = to_string(sle->key()); - obj[jss::tx_blob] = serializeHex(*sle); - } - else if (expanded) - array.append(sle->getJson(JsonOptions::none)); - else - array.append(to_string(sle->key())); + auto&& obj = appendObject(array); + obj[jss::hash] = to_string(sle->key()); + obj[jss::tx_blob] = serializeHex(*sle); } + else if (expanded) + array.append(sle->getJson(JsonOptions::none)); + else + array.append(to_string(sle->key())); } } diff --git a/src/xrpld/app/ledger/detail/OpenLedger.cpp b/src/xrpld/app/ledger/detail/OpenLedger.cpp index 86a3b4b840..2c98caaa6d 100644 --- a/src/xrpld/app/ledger/detail/OpenLedger.cpp +++ b/src/xrpld/app/ledger/detail/OpenLedger.cpp @@ -26,6 +26,8 @@ #include #include +#include + #include namespace ripple { @@ -120,6 +122,18 @@ OpenLedger::accept( { auto const& tx = txpair.first; auto const txId = tx->getTransactionID(); + + // skip batch txns + // LCOV_EXCL_START + if (tx->isFlag(tfInnerBatchTxn) && rules.enabled(featureBatch)) + { + XRPL_ASSERT( + txpair.second && txpair.second->isFieldPresent(sfParentBatchID), + "Inner Batch transaction missing sfParentBatchID"); + continue; + } + // LCOV_EXCL_STOP + if (auto const toSkip = app.getHashRouter().shouldRelay(txId)) { JLOG(j_.debug()) << "Relaying recovered tx " << txId; diff --git a/src/xrpld/app/main/Application.cpp b/src/xrpld/app/main/Application.cpp index 5d495aaf06..8e893a655e 100644 --- a/src/xrpld/app/main/Application.cpp +++ b/src/xrpld/app/main/Application.cpp @@ -256,8 +256,8 @@ public: if ((cores == 1) || ((config.NODE_SIZE == 0) && (cores == 2))) return 1; - // Otherwise, prefer two threads. - return 2; + // Otherwise, prefer six threads. + return 6; #endif } @@ -279,13 +279,14 @@ public: , m_journal(logs_->journal("Application")) // PerfLog must be started before any other threads are launched. - , perfLog_(perf::make_PerfLog( - perf::setup_PerfLog( - config_->section("perf"), - config_->CONFIG_DIR), - *this, - logs_->journal("PerfLog"), - [this] { signalStop(); })) + , perfLog_( + perf::make_PerfLog( + perf::setup_PerfLog( + config_->section("perf"), + config_->CONFIG_DIR), + *this, + logs_->journal("PerfLog"), + [this] { signalStop("PerfLog"); })) , m_txMaster(*this) @@ -293,33 +294,34 @@ public: config_->section(SECTION_INSIGHT), logs_->journal("Collector"))) - , m_jobQueue(std::make_unique( - [](std::unique_ptr const& config) { - if (config->standalone() && !config->FORCE_MULTI_THREAD) - return 1; + , m_jobQueue( + std::make_unique( + [](std::unique_ptr const& config) { + if (config->standalone() && !config->FORCE_MULTI_THREAD) + return 1; - if (config->WORKERS) - return config->WORKERS; + if (config->WORKERS) + return config->WORKERS; - auto count = - static_cast(std::thread::hardware_concurrency()); + auto count = + static_cast(std::thread::hardware_concurrency()); - // Be more aggressive about the number of threads to use - // for the job queue if the server is configured as "large" - // or "huge" if there are enough cores. - if (config->NODE_SIZE >= 4 && count >= 16) - count = 6 + std::min(count, 8); - else if (config->NODE_SIZE >= 3 && count >= 8) - count = 4 + std::min(count, 6); - else - count = 2 + std::min(count, 4); + // Be more aggressive about the number of threads to use + // for the job queue if the server is configured as + // "large" or "huge" if there are enough cores. + if (config->NODE_SIZE >= 4 && count >= 16) + count = 6 + std::min(count, 8); + else if (config->NODE_SIZE >= 3 && count >= 8) + count = 4 + std::min(count, 6); + else + count = 2 + std::min(count, 4); - return count; - }(config_), - m_collectorManager->group("jobq"), - logs_->journal("JobQueue"), - *logs_, - *perfLog_)) + return count; + }(config_), + m_collectorManager->group("jobq"), + logs_->journal("JobQueue"), + *logs_, + *perfLog_)) , m_nodeStoreScheduler(*m_jobQueue) @@ -344,9 +346,10 @@ public: , validatorKeys_(*config_, m_journal) - , m_resourceManager(Resource::make_Manager( - m_collectorManager->collector(), - logs_->journal("Resource"))) + , m_resourceManager( + Resource::make_Manager( + m_collectorManager->collector(), + logs_->journal("Resource"))) , m_nodeStore(m_shaMapStore->makeNodeStore( config_->PREFETCH_WORKERS > 0 ? config_->PREFETCH_WORKERS : 4)) @@ -355,16 +358,18 @@ public: , m_orderBookDB(*this) - , m_pathRequests(std::make_unique( - *this, - logs_->journal("PathRequest"), - m_collectorManager->collector())) + , m_pathRequests( + std::make_unique( + *this, + logs_->journal("PathRequest"), + m_collectorManager->collector())) - , m_ledgerMaster(std::make_unique( - *this, - stopwatch(), - m_collectorManager->collector(), - logs_->journal("LedgerMaster"))) + , m_ledgerMaster( + std::make_unique( + *this, + stopwatch(), + m_collectorManager->collector(), + logs_->journal("LedgerMaster"))) , ledgerCleaner_( make_LedgerCleaner(*this, logs_->journal("LedgerCleaner"))) @@ -384,10 +389,11 @@ public: gotTXSet(set, fromAcquire); })) - , m_ledgerReplayer(std::make_unique( - *this, - *m_inboundLedgers, - make_PeerSetBuilder(*this))) + , m_ledgerReplayer( + std::make_unique( + *this, + *m_inboundLedgers, + make_PeerSetBuilder(*this))) , m_acceptedLedgerCache( "AcceptedLedger", @@ -411,8 +417,9 @@ public: , cluster_(std::make_unique(logs_->journal("Overlay"))) - , peerReservations_(std::make_unique( - logs_->journal("PeerReservationTable"))) + , peerReservations_( + std::make_unique( + logs_->journal("PeerReservationTable"))) , validatorManifests_( std::make_unique(logs_->journal("ManifestCache"))) @@ -420,13 +427,14 @@ public: , publisherManifests_( std::make_unique(logs_->journal("ManifestCache"))) - , validators_(std::make_unique( - *validatorManifests_, - *publisherManifests_, - *timeKeeper_, - config_->legacy("database_path"), - logs_->journal("ValidatorList"), - config_->VALIDATION_QUORUM)) + , validators_( + std::make_unique( + *validatorManifests_, + *publisherManifests_, + *timeKeeper_, + config_->legacy("database_path"), + logs_->journal("ValidatorList"), + config_->VALIDATION_QUORUM)) , validatorSites_(std::make_unique(*this)) @@ -441,9 +449,10 @@ public: , mFeeTrack( std::make_unique(logs_->journal("LoadManager"))) - , hashRouter_(std::make_unique( - setup_HashRouter(*config_), - stopwatch())) + , hashRouter_( + std::make_unique( + setup_HashRouter(*config_), + stopwatch())) , mValidations( ValidationParms(), @@ -505,7 +514,7 @@ public: void run() override; void - signalStop(std::string msg = "") override; + signalStop(std::string msg) override; bool checkSigs() const override; void @@ -977,7 +986,7 @@ public: if (!config_->standalone() && !getRelationalDatabase().transactionDbHasSpace(*config_)) { - signalStop(); + signalStop("Out of transaction DB space"); } // VFALCO NOTE Does the order of calls matter? @@ -1193,7 +1202,7 @@ ApplicationImp::setup(boost::program_options::variables_map const& cmdline) JLOG(m_journal.info()) << "Received signal " << signum; if (signum == SIGTERM || signum == SIGINT) - signalStop(); + signalStop("Signal: " + to_string(signum)); }); auto debug_log = config_->getDebugLogFile(); diff --git a/src/xrpld/app/main/Application.h b/src/xrpld/app/main/Application.h index f3cff35d4b..36477cb75c 100644 --- a/src/xrpld/app/main/Application.h +++ b/src/xrpld/app/main/Application.h @@ -141,7 +141,7 @@ public: virtual void run() = 0; virtual void - signalStop(std::string msg = "") = 0; + signalStop(std::string msg) = 0; virtual bool checkSigs() const = 0; virtual void diff --git a/src/xrpld/app/misc/AMMHelpers.h b/src/xrpld/app/misc/AMMHelpers.h index 97554b7e15..8cc39468b1 100644 --- a/src/xrpld/app/misc/AMMHelpers.h +++ b/src/xrpld/app/misc/AMMHelpers.h @@ -48,6 +48,8 @@ reduceOffer(auto const& amount) } // namespace detail +enum class IsDeposit : bool { No = false, Yes = true }; + /** 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 @@ -67,7 +69,7 @@ ammLPTokens( * @return tokens */ STAmount -lpTokensIn( +lpTokensOut( STAmount const& asset1Balance, STAmount const& asset1Deposit, STAmount const& lptAMMBalance, @@ -96,7 +98,7 @@ ammAssetIn( * @return tokens out amount */ STAmount -lpTokensOut( +lpTokensIn( STAmount const& asset1Balance, STAmount const& asset1Withdraw, STAmount const& lptAMMBalance, @@ -110,7 +112,7 @@ lpTokensOut( * @return calculated asset amount */ STAmount -withdrawByTokens( +ammAssetOut( STAmount const& assetBalance, STAmount const& lptAMMBalance, STAmount const& lpTokens, @@ -608,13 +610,13 @@ square(Number const& n); * withdraw to cancel out the precision loss. * @param lptAMMBalance LPT AMM Balance * @param lpTokens LP tokens to deposit or withdraw - * @param isDeposit true if deposit, false if withdraw + * @param isDeposit Yes if deposit, No if withdraw */ STAmount adjustLPTokens( STAmount const& lptAMMBalance, STAmount const& lpTokens, - bool isDeposit); + IsDeposit isDeposit); /** Calls adjustLPTokens() and adjusts deposit or withdraw amounts if * the adjusted LP tokens are less than the provided LP tokens. @@ -624,7 +626,7 @@ adjustLPTokens( * @param lptAMMBalance LPT AMM Balance * @param lpTokens LP tokens to deposit or withdraw * @param tfee trading fee in basis points - * @param isDeposit true if deposit, false if withdraw + * @param isDeposit Yes if deposit, No if withdraw * @return */ std::tuple, STAmount> @@ -635,7 +637,7 @@ adjustAmountsByLPTokens( STAmount const& lptAMMBalance, STAmount const& lpTokens, std::uint16_t tfee, - bool isDeposit); + IsDeposit isDeposit); /** Positive solution for quadratic equation: * x = (-b + sqrt(b**2 + 4*a*c))/(2*a) @@ -643,6 +645,141 @@ adjustAmountsByLPTokens( Number solveQuadraticEq(Number const& a, Number const& b, Number const& c); +STAmount +multiply(STAmount const& amount, Number const& frac, Number::rounding_mode rm); + +namespace detail { + +inline Number::rounding_mode +getLPTokenRounding(IsDeposit isDeposit) +{ + // Minimize on deposit, maximize on withdraw to ensure + // AMM invariant sqrt(poolAsset1 * poolAsset2) >= LPTokensBalance + return isDeposit == IsDeposit::Yes ? Number::downward : Number::upward; +} + +inline Number::rounding_mode +getAssetRounding(IsDeposit isDeposit) +{ + // Maximize on deposit, minimize on withdraw to ensure + // AMM invariant sqrt(poolAsset1 * poolAsset2) >= LPTokensBalance + return isDeposit == IsDeposit::Yes ? Number::upward : Number::downward; +} + +} // namespace detail + +/** Round AMM equal deposit/withdrawal amount. Deposit/withdrawal formulas + * calculate the amount as a fractional value of the pool balance. The rounding + * takes place on the last step of multiplying the balance by the fraction if + * AMMv1_3 is enabled. + */ +template +STAmount +getRoundedAsset( + Rules const& rules, + STAmount const& balance, + A const& frac, + IsDeposit isDeposit) +{ + if (!rules.enabled(fixAMMv1_3)) + { + if constexpr (std::is_same_v) + return multiply(balance, frac, balance.issue()); + else + return toSTAmount(balance.issue(), balance * frac); + } + auto const rm = detail::getAssetRounding(isDeposit); + return multiply(balance, frac, rm); +} + +/** Round AMM single deposit/withdrawal amount. + * The lambda's are used to delay evaluation until the function + * is executed so that the calculation is not done twice. noRoundCb() is + * called if AMMv1_3 is disabled. Otherwise, the rounding is set and + * the amount is: + * isDeposit is Yes - the balance multiplied by productCb() + * isDeposit is No - the result of productCb(). The rounding is + * the same for all calculations in productCb() + */ +STAmount +getRoundedAsset( + Rules const& rules, + std::function&& noRoundCb, + STAmount const& balance, + std::function&& productCb, + IsDeposit isDeposit); + +/** Round AMM deposit/withdrawal LPToken amount. Deposit/withdrawal formulas + * calculate the lptokens as a fractional value of the AMM total lptokens. + * The rounding takes place on the last step of multiplying the balance by + * the fraction if AMMv1_3 is enabled. The tokens are then + * adjusted to factor in the loss in precision (we only keep 16 significant + * digits) when adding the lptokens to the balance. + */ +STAmount +getRoundedLPTokens( + Rules const& rules, + STAmount const& balance, + Number const& frac, + IsDeposit isDeposit); + +/** Round AMM single deposit/withdrawal LPToken amount. + * The lambda's are used to delay evaluation until the function is executed + * so that the calculations are not done twice. + * noRoundCb() is called if AMMv1_3 is disabled. Otherwise, the rounding is set + * and the lptokens are: + * if isDeposit is Yes - the result of productCb(). The rounding is + * the same for all calculations in productCb() + * if isDeposit is No - the balance multiplied by productCb() + * The lptokens are then adjusted to factor in the loss in precision + * (we only keep 16 significant digits) when adding the lptokens to the balance. + */ +STAmount +getRoundedLPTokens( + Rules const& rules, + std::function&& noRoundCb, + STAmount const& lptAMMBalance, + std::function&& productCb, + IsDeposit isDeposit); + +/* Next two functions adjust asset in/out amount to factor in the adjusted + * lptokens. The lptokens are calculated from the asset in/out. The lptokens are + * then adjusted to factor in the loss in precision. The adjusted lptokens might + * be less than the initially calculated tokens. Therefore, the asset in/out + * must be adjusted. The rounding might result in the adjusted amount being + * greater than the original asset in/out amount. If this happens, + * then the original amount is reduced by the difference in the adjusted amount + * and the original amount. The actual tokens and the actual adjusted amount + * are then recalculated. The minimum of the original and the actual + * adjusted amount is returned. + */ +std::pair +adjustAssetInByTokens( + Rules const& rules, + STAmount const& balance, + STAmount const& amount, + STAmount const& lptAMMBalance, + STAmount const& tokens, + std::uint16_t tfee); +std::pair +adjustAssetOutByTokens( + Rules const& rules, + STAmount const& balance, + STAmount const& amount, + STAmount const& lptAMMBalance, + STAmount const& tokens, + std::uint16_t tfee); + +/** Find a fraction of tokens after the tokens are adjusted. The fraction + * is used to adjust equal deposit/withdraw amount. + */ +Number +adjustFracByTokens( + Rules const& rules, + STAmount const& lptAMMBalance, + STAmount const& tokens, + Number const& frac); + } // namespace ripple #endif // RIPPLE_APP_MISC_AMMHELPERS_H_INCLUDED diff --git a/src/xrpld/app/misc/AMMUtils.h b/src/xrpld/app/misc/AMMUtils.h index b2c0007dc7..2a9f82ae60 100644 --- a/src/xrpld/app/misc/AMMUtils.h +++ b/src/xrpld/app/misc/AMMUtils.h @@ -125,6 +125,17 @@ isOnlyLiquidityProvider( Issue const& ammIssue, AccountID const& lpAccount); +/** Due to rounding, the LPTokenBalance of the last LP might + * not match the LP's trustline balance. If it's within the tolerance, + * update LPTokenBalance to match the LP's trustline balance. + */ +Expected +verifyAndAdjustLPTokenBalance( + Sandbox& sb, + STAmount const& lpTokens, + std::shared_ptr& ammSle, + AccountID const& account); + } // namespace ripple #endif // RIPPLE_APP_MISC_AMMUTILS_H_INCLUDED diff --git a/src/xrpld/app/misc/CredentialHelpers.cpp b/src/xrpld/app/misc/CredentialHelpers.cpp index 03ad1f9c80..6d1f9f78c5 100644 --- a/src/xrpld/app/misc/CredentialHelpers.cpp +++ b/src/xrpld/app/misc/CredentialHelpers.cpp @@ -120,15 +120,15 @@ deleteSLE( } NotTEC -checkFields(PreflightContext const& ctx) +checkFields(STTx const& tx, beast::Journal j) { - if (!ctx.tx.isFieldPresent(sfCredentialIDs)) + if (!tx.isFieldPresent(sfCredentialIDs)) return tesSUCCESS; - auto const& credentials = ctx.tx.getFieldV256(sfCredentialIDs); + auto const& credentials = tx.getFieldV256(sfCredentialIDs); if (credentials.empty() || (credentials.size() > maxCredentialsArraySize)) { - JLOG(ctx.j.trace()) + JLOG(j.trace()) << "Malformed transaction: Credentials array size is invalid: " << credentials.size(); return temMALFORMED; @@ -140,7 +140,7 @@ checkFields(PreflightContext const& ctx) auto [it, ins] = duplicates.insert(cred); if (!ins) { - JLOG(ctx.j.trace()) + JLOG(j.trace()) << "Malformed transaction: duplicates in credentials."; return temMALFORMED; } @@ -150,24 +150,28 @@ checkFields(PreflightContext const& ctx) } TER -valid(PreclaimContext const& ctx, AccountID const& src) +valid( + STTx const& tx, + ReadView const& view, + AccountID const& src, + beast::Journal j) { - if (!ctx.tx.isFieldPresent(sfCredentialIDs)) + if (!tx.isFieldPresent(sfCredentialIDs)) return tesSUCCESS; - auto const& credIDs(ctx.tx.getFieldV256(sfCredentialIDs)); + auto const& credIDs(tx.getFieldV256(sfCredentialIDs)); for (auto const& h : credIDs) { - auto const sleCred = ctx.view.read(keylet::credential(h)); + auto const sleCred = view.read(keylet::credential(h)); if (!sleCred) { - JLOG(ctx.j.trace()) << "Credential doesn't exist. Cred: " << h; + JLOG(j.trace()) << "Credential doesn't exist. Cred: " << h; return tecBAD_CREDENTIALS; } if (sleCred->getAccountID(sfSubject) != src) { - JLOG(ctx.j.trace()) + JLOG(j.trace()) << "Credential doesn't belong to the source account. Cred: " << h; return tecBAD_CREDENTIALS; @@ -175,7 +179,7 @@ valid(PreclaimContext const& ctx, AccountID const& src) if (!(sleCred->getFlags() & lsfAccepted)) { - JLOG(ctx.j.trace()) << "Credential isn't accepted. Cred: " << h; + JLOG(j.trace()) << "Credential isn't accepted. Cred: " << h; return tecBAD_CREDENTIALS; } @@ -336,9 +340,7 @@ verifyValidDomain( credentials.push_back(keyletCredential.key); } - // Result intentionally ignored. - [[maybe_unused]] bool _ = credentials::removeExpired(view, credentials, j); - + bool const foundExpired = credentials::removeExpired(view, credentials, j); for (auto const& h : credentials) { auto sleCredential = view.read(keylet::credential(h)); @@ -349,15 +351,17 @@ verifyValidDomain( return tesSUCCESS; } - return tecNO_PERMISSION; + return foundExpired ? tecEXPIRED : tecNO_PERMISSION; } TER verifyDepositPreauth( - ApplyContext& ctx, + STTx const& tx, + ApplyView& view, AccountID const& src, AccountID const& dst, - std::shared_ptr const& sleDst) + std::shared_ptr const& sleDst, + beast::Journal j) { // If depositPreauth is enabled, then an account that requires // authorization has at least two ways to get a payment in: @@ -365,24 +369,21 @@ verifyDepositPreauth( // 2. If src is deposit preauthorized by dst (either by account or by // credentials). - bool const credentialsPresent = ctx.tx.isFieldPresent(sfCredentialIDs); + bool const credentialsPresent = tx.isFieldPresent(sfCredentialIDs); if (credentialsPresent && - credentials::removeExpired( - ctx.view(), ctx.tx.getFieldV256(sfCredentialIDs), ctx.journal)) + credentials::removeExpired(view, tx.getFieldV256(sfCredentialIDs), j)) return tecEXPIRED; if (sleDst && (sleDst->getFlags() & lsfDepositAuth)) { if (src != dst) { - if (!ctx.view().exists(keylet::depositPreauth(dst, src))) + if (!view.exists(keylet::depositPreauth(dst, src))) return !credentialsPresent ? tecNO_PERMISSION : credentials::authorizedDepositPreauth( - ctx.view(), - ctx.tx.getFieldV256(sfCredentialIDs), - dst); + view, tx.getFieldV256(sfCredentialIDs), dst); } } diff --git a/src/xrpld/app/misc/CredentialHelpers.h b/src/xrpld/app/misc/CredentialHelpers.h index 162ddd6515..84938180ce 100644 --- a/src/xrpld/app/misc/CredentialHelpers.h +++ b/src/xrpld/app/misc/CredentialHelpers.h @@ -20,7 +20,16 @@ #ifndef RIPPLE_APP_MISC_CREDENTIALHELPERS_H_INCLUDED #define RIPPLE_APP_MISC_CREDENTIALHELPERS_H_INCLUDED -#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include namespace ripple { namespace credentials { @@ -48,13 +57,17 @@ deleteSLE( // Amendment and parameters checks for sfCredentialIDs field NotTEC -checkFields(PreflightContext const& ctx); +checkFields(STTx const& tx, beast::Journal j); // Accessing the ledger to check if provided credentials are valid. Do not use // in doApply (only in preclaim) since it does not remove expired credentials. // If you call it in prelaim, you also must call verifyDepositPreauth in doApply TER -valid(PreclaimContext const& ctx, AccountID const& src); +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 // in preclaim and it returns tecEXPIRED, you should call verifyValidDomain in @@ -93,10 +106,12 @@ verifyValidDomain( // Check expired credentials and for existing DepositPreauth ledger object TER verifyDepositPreauth( - ApplyContext& ctx, + STTx const& tx, + ApplyView& view, AccountID const& src, AccountID const& dst, - std::shared_ptr const& sleDst); + std::shared_ptr const& sleDst, + beast::Journal j); } // namespace ripple diff --git a/src/xrpld/app/misc/DelegateUtils.h b/src/xrpld/app/misc/DelegateUtils.h index cad3bed376..8d657e6a09 100644 --- a/src/xrpld/app/misc/DelegateUtils.h +++ b/src/xrpld/app/misc/DelegateUtils.h @@ -31,7 +31,8 @@ namespace ripple { * Check if the delegate account has permission to execute the transaction. * @param delegate The delegate account. * @param tx The transaction that the delegate account intends to execute. - * @return tesSUCCESS if the transaction is allowed, tecNO_PERMISSION if not. + * @return tesSUCCESS if the transaction is allowed, tecNO_DELEGATE_PERMISSION + * if not. */ TER checkTxPermission(std::shared_ptr const& delegate, STTx const& tx); diff --git a/src/xrpld/app/misc/HashRouter.h b/src/xrpld/app/misc/HashRouter.h index a13bcb9f8f..d1d69623c1 100644 --- a/src/xrpld/app/misc/HashRouter.h +++ b/src/xrpld/app/misc/HashRouter.h @@ -27,6 +27,7 @@ #include #include +#include namespace ripple { diff --git a/src/xrpld/app/misc/NetworkOPs.cpp b/src/xrpld/app/misc/NetworkOPs.cpp index d87dea3c52..a7ddbe912c 100644 --- a/src/xrpld/app/misc/NetworkOPs.cpp +++ b/src/xrpld/app/misc/NetworkOPs.cpp @@ -63,7 +63,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -1190,6 +1192,15 @@ NetworkOPsImp::submitTransaction(std::shared_ptr const& iTrans) return; } + // Enforce Network bar for batch txn + if (iTrans->isFlag(tfInnerBatchTxn) && + m_ledgerMaster.getValidatedRules().enabled(featureBatch)) + { + JLOG(m_journal.error()) + << "Submitted transaction invalid: tfInnerBatchTxn flag present."; + return; + } + // this is an asynchronous interface auto const trans = sterilize(*iTrans); @@ -1249,15 +1260,25 @@ NetworkOPsImp::preProcessTransaction(std::shared_ptr& transaction) return false; } + auto const view = m_ledgerMaster.getCurrentLedger(); + + // This function is called by several different parts of the codebase + // under no circumstances will we ever accept an inner txn within a batch + // txn from the network. + auto const sttx = *transaction->getSTransaction(); + if (sttx.isFlag(tfInnerBatchTxn) && view->rules().enabled(featureBatch)) + { + transaction->setStatus(INVALID); + transaction->setResult(temINVALID_FLAG); + app_.getHashRouter().setFlags(transaction->getID(), SF_BAD); + return false; + } + // NOTE eahennis - I think this check is redundant, // but I'm not 100% sure yet. // If so, only cost is looking up HashRouter flags. - auto const view = m_ledgerMaster.getCurrentLedger(); - auto const [validity, reason] = checkValidity( - app_.getHashRouter(), - *transaction->getSTransaction(), - view->rules(), - app_.config()); + auto const [validity, reason] = + checkValidity(app_.getHashRouter(), sttx, view->rules(), app_.config()); XRPL_ASSERT( validity == Validity::Valid, "ripple::NetworkOPsImp::processTransaction : valid validity"); @@ -1659,13 +1680,17 @@ NetworkOPsImp::apply(std::unique_lock& batchLock) { auto const toSkip = app_.getHashRouter().shouldRelay(e.transaction->getID()); - - if (toSkip) + if (auto const sttx = *(e.transaction->getSTransaction()); + toSkip && + // Skip relaying if it's an inner batch txn and batch + // feature is enabled + !(sttx.isFlag(tfInnerBatchTxn) && + newOL->rules().enabled(featureBatch))) { protocol::TMTransaction tx; Serializer s; - e.transaction->getSTransaction()->add(s); + sttx.add(s); tx.set_rawtransaction(s.data(), s.size()); tx.set_status(protocol::tsCURRENT); tx.set_receivetimestamp( @@ -3020,6 +3045,11 @@ NetworkOPsImp::pubProposedTransaction( std::shared_ptr const& transaction, TER result) { + // never publish an inner txn inside a batch txn + if (transaction->isFlag(tfInnerBatchTxn) && + ledger->rules().enabled(featureBatch)) + return; + MultiApiJson jvObj = transJson(transaction, result, false, ledger, std::nullopt); @@ -3229,6 +3259,7 @@ NetworkOPsImp::transJson( jvObj[jss::meta] = meta->get().getJson(JsonOptions::none); RPC::insertDeliveredAmount( jvObj[jss::meta], *ledger, transaction, meta->get()); + RPC::insertNFTSyntheticInJson(jvObj, transaction, meta->get()); RPC::insertMPTokenIssuanceID( jvObj[jss::meta], transaction, meta->get()); } diff --git a/src/xrpld/app/misc/PermissionedDEXHelpers.cpp b/src/xrpld/app/misc/PermissionedDEXHelpers.cpp new file mode 100644 index 0000000000..13793387c8 --- /dev/null +++ b/src/xrpld/app/misc/PermissionedDEXHelpers.cpp @@ -0,0 +1,91 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include + +namespace ripple { +namespace permissioned_dex { + +bool +accountInDomain( + ReadView const& view, + AccountID const& account, + Domain const& domainID) +{ + auto const sleDomain = view.read(keylet::permissionedDomain(domainID)); + if (!sleDomain) + return false; + + // domain owner is in the domain + if (sleDomain->getAccountID(sfOwner) == account) + return true; + + auto const& credentials = sleDomain->getFieldArray(sfAcceptedCredentials); + + bool const inDomain = std::any_of( + credentials.begin(), credentials.end(), [&](auto const& credential) { + auto const sleCred = view.read( + keylet::credential( + account, + credential[sfIssuer], + credential[sfCredentialType])); + if (!sleCred || !sleCred->isFlag(lsfAccepted)) + return false; + + return !credentials::checkExpired( + sleCred, view.info().parentCloseTime); + }); + + return inDomain; +} + +bool +offerInDomain( + ReadView const& view, + uint256 const& offerID, + Domain const& domainID, + beast::Journal j) +{ + auto const sleOffer = view.read(keylet::offer(offerID)); + + // The following are defensive checks that should never happen, since this + // function is used to check against the order book offers, which should not + // have any of the following wrong behavior + if (!sleOffer) + return false; // LCOV_EXCL_LINE + if (!sleOffer->isFieldPresent(sfDomainID)) + return false; // LCOV_EXCL_LINE + if (sleOffer->getFieldH256(sfDomainID) != domainID) + return false; // LCOV_EXCL_LINE + + if (sleOffer->isFlag(lsfHybrid) && + !sleOffer->isFieldPresent(sfAdditionalBooks)) + { + JLOG(j.error()) << "Hybrid offer " << offerID + << " missing AdditionalBooks field"; + return false; // LCOV_EXCL_LINE + } + + return accountInDomain(view, sleOffer->getAccountID(sfAccount), domainID); +} + +} // namespace permissioned_dex + +} // namespace ripple diff --git a/src/xrpld/app/misc/PermissionedDEXHelpers.h b/src/xrpld/app/misc/PermissionedDEXHelpers.h new file mode 100644 index 0000000000..1b3a0323fd --- /dev/null +++ b/src/xrpld/app/misc/PermissionedDEXHelpers.h @@ -0,0 +1,43 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once +#include + +namespace ripple { +namespace permissioned_dex { + +// Check if an account is in a permissioned domain +[[nodiscard]] bool +accountInDomain( + ReadView const& view, + AccountID const& account, + Domain const& domainID); + +// Check if an offer is in the permissioned domain +[[nodiscard]] bool +offerInDomain( + ReadView const& view, + uint256 const& offerID, + Domain const& domainID, + beast::Journal j); + +} // namespace permissioned_dex + +} // namespace ripple diff --git a/src/xrpld/app/misc/ValidatorList.h b/src/xrpld/app/misc/ValidatorList.h index 4cb32282db..1f5d728824 100644 --- a/src/xrpld/app/misc/ValidatorList.h +++ b/src/xrpld/app/misc/ValidatorList.h @@ -226,7 +226,7 @@ class ValidatorList TimeKeeper& timeKeeper_; boost::filesystem::path const dataPath_; beast::Journal const j_; - boost::shared_mutex mutable mutex_; + std::shared_mutex mutable mutex_; using lock_guard = std::lock_guard; using shared_lock = std::shared_lock; diff --git a/src/xrpld/app/misc/detail/AMMHelpers.cpp b/src/xrpld/app/misc/detail/AMMHelpers.cpp index 8724c413a6..49ad01c3ae 100644 --- a/src/xrpld/app/misc/detail/AMMHelpers.cpp +++ b/src/xrpld/app/misc/detail/AMMHelpers.cpp @@ -27,6 +27,10 @@ ammLPTokens( STAmount const& asset2, Issue const& lptIssue) { + // AMM invariant: sqrt(asset1 * asset2) >= LPTokensBalance + auto const rounding = + isFeatureEnabled(fixAMMv1_3) ? Number::downward : Number::getround(); + NumberRoundModeGuard g(rounding); auto const tokens = root2(asset1 * asset2); return toSTAmount(lptIssue, tokens); } @@ -38,7 +42,7 @@ ammLPTokens( * where f1 = 1 - tfee, f2 = (1 - tfee/2)/f1 */ STAmount -lpTokensIn( +lpTokensOut( STAmount const& asset1Balance, STAmount const& asset1Deposit, STAmount const& lptAMMBalance, @@ -48,8 +52,17 @@ lpTokensIn( auto const f2 = feeMultHalf(tfee) / f1; Number const r = asset1Deposit / asset1Balance; auto const c = root2(f2 * f2 + r / f1) - f2; - auto const t = lptAMMBalance * (r - c) / (1 + c); - return toSTAmount(lptAMMBalance.issue(), t); + if (!isFeatureEnabled(fixAMMv1_3)) + { + auto const t = lptAMMBalance * (r - c) / (1 + c); + return toSTAmount(lptAMMBalance.issue(), t); + } + else + { + // minimize tokens out + auto const frac = (r - c) / (1 + c); + return multiply(lptAMMBalance, frac, Number::downward); + } } /* Equation 4 solves equation 3 for b: @@ -78,8 +91,17 @@ ammAssetIn( auto const a = 1 / (t2 * t2); auto const b = 2 * d / t2 - 1 / f1; auto const c = d * d - f2 * f2; - return toSTAmount( - asset1Balance.issue(), asset1Balance * solveQuadraticEq(a, b, c)); + if (!isFeatureEnabled(fixAMMv1_3)) + { + return toSTAmount( + asset1Balance.issue(), asset1Balance * solveQuadraticEq(a, b, c)); + } + else + { + // maximize deposit + auto const frac = solveQuadraticEq(a, b, c); + return multiply(asset1Balance, frac, Number::upward); + } } /* Equation 7: @@ -87,7 +109,7 @@ ammAssetIn( * where R = b/B, c = R*fee + 2 - fee */ STAmount -lpTokensOut( +lpTokensIn( STAmount const& asset1Balance, STAmount const& asset1Withdraw, STAmount const& lptAMMBalance, @@ -96,8 +118,17 @@ lpTokensOut( Number const fr = asset1Withdraw / asset1Balance; auto const f1 = getFee(tfee); auto const c = fr * f1 + 2 - f1; - auto const t = lptAMMBalance * (c - root2(c * c - 4 * fr)) / 2; - return toSTAmount(lptAMMBalance.issue(), t); + if (!isFeatureEnabled(fixAMMv1_3)) + { + auto const t = lptAMMBalance * (c - root2(c * c - 4 * fr)) / 2; + return toSTAmount(lptAMMBalance.issue(), t); + } + else + { + // maximize tokens in + auto const frac = (c - root2(c * c - 4 * fr)) / 2; + return multiply(lptAMMBalance, frac, Number::upward); + } } /* Equation 8 solves equation 7 for b: @@ -111,7 +142,7 @@ lpTokensOut( * R = (t1**2 + t1*(f - 2)) / (t1*f - 1) */ STAmount -withdrawByTokens( +ammAssetOut( STAmount const& assetBalance, STAmount const& lptAMMBalance, STAmount const& lpTokens, @@ -119,8 +150,17 @@ withdrawByTokens( { auto const f = getFee(tfee); Number const t1 = lpTokens / lptAMMBalance; - auto const b = assetBalance * (t1 * t1 - t1 * (2 - f)) / (t1 * f - 1); - return toSTAmount(assetBalance.issue(), b); + if (!isFeatureEnabled(fixAMMv1_3)) + { + auto const b = assetBalance * (t1 * t1 - t1 * (2 - f)) / (t1 * f - 1); + return toSTAmount(assetBalance.issue(), b); + } + else + { + // minimize withdraw + auto const frac = (t1 * t1 - t1 * (2 - f)) / (t1 * f - 1); + return multiply(assetBalance, frac, Number::downward); + } } Number @@ -133,12 +173,12 @@ STAmount adjustLPTokens( STAmount const& lptAMMBalance, STAmount const& lpTokens, - bool isDeposit) + IsDeposit isDeposit) { // Force rounding downward to ensure adjusted tokens are less or equal // to requested tokens. saveNumberRoundMode rm(Number::setround(Number::rounding_mode::downward)); - if (isDeposit) + if (isDeposit == IsDeposit::Yes) return (lptAMMBalance + lpTokens) - lptAMMBalance; return (lpTokens - lptAMMBalance) + lptAMMBalance; } @@ -151,8 +191,12 @@ adjustAmountsByLPTokens( STAmount const& lptAMMBalance, STAmount const& lpTokens, std::uint16_t tfee, - bool isDeposit) + IsDeposit isDeposit) { + // AMMv1_3 amendment adjusts tokens and amounts in deposit/withdraw + if (isFeatureEnabled(fixAMMv1_3)) + return std::make_tuple(amount, amount2, lpTokens); + auto const lpTokensActual = adjustLPTokens(lptAMMBalance, lpTokens, isDeposit); @@ -191,14 +235,14 @@ adjustAmountsByLPTokens( // Single trade auto const amountActual = [&]() { - if (isDeposit) + if (isDeposit == IsDeposit::Yes) return ammAssetIn( amountBalance, lptAMMBalance, lpTokensActual, tfee); else if (!ammRoundingEnabled) - return withdrawByTokens( + return ammAssetOut( amountBalance, lptAMMBalance, lpTokens, tfee); else - return withdrawByTokens( + return ammAssetOut( amountBalance, lptAMMBalance, lpTokensActual, tfee); }(); if (!ammRoundingEnabled) @@ -237,4 +281,132 @@ solveQuadraticEqSmallest(Number const& a, Number const& b, Number const& c) return (2 * c) / (-b + root2(d)); } +STAmount +multiply(STAmount const& amount, Number const& frac, Number::rounding_mode rm) +{ + NumberRoundModeGuard g(rm); + auto const t = amount * frac; + return toSTAmount(amount.issue(), t, rm); +} + +STAmount +getRoundedAsset( + Rules const& rules, + std::function&& noRoundCb, + STAmount const& balance, + std::function&& productCb, + IsDeposit isDeposit) +{ + if (!rules.enabled(fixAMMv1_3)) + return toSTAmount(balance.issue(), noRoundCb()); + + auto const rm = detail::getAssetRounding(isDeposit); + if (isDeposit == IsDeposit::Yes) + return multiply(balance, productCb(), rm); + NumberRoundModeGuard g(rm); + return toSTAmount(balance.issue(), productCb(), rm); +} + +STAmount +getRoundedLPTokens( + Rules const& rules, + STAmount const& balance, + Number const& frac, + IsDeposit isDeposit) +{ + if (!rules.enabled(fixAMMv1_3)) + return toSTAmount(balance.issue(), balance * frac); + + auto const rm = detail::getLPTokenRounding(isDeposit); + auto const tokens = multiply(balance, frac, rm); + return adjustLPTokens(balance, tokens, isDeposit); +} + +STAmount +getRoundedLPTokens( + Rules const& rules, + std::function&& noRoundCb, + STAmount const& lptAMMBalance, + std::function&& productCb, + IsDeposit isDeposit) +{ + if (!rules.enabled(fixAMMv1_3)) + return toSTAmount(lptAMMBalance.issue(), noRoundCb()); + + auto const tokens = [&] { + auto const rm = detail::getLPTokenRounding(isDeposit); + if (isDeposit == IsDeposit::Yes) + { + NumberRoundModeGuard g(rm); + return toSTAmount(lptAMMBalance.issue(), productCb(), rm); + } + return multiply(lptAMMBalance, productCb(), rm); + }(); + return adjustLPTokens(lptAMMBalance, tokens, isDeposit); +} + +std::pair +adjustAssetInByTokens( + Rules const& rules, + STAmount const& balance, + STAmount const& amount, + STAmount const& lptAMMBalance, + STAmount const& tokens, + std::uint16_t tfee) +{ + if (!rules.enabled(fixAMMv1_3)) + return {tokens, amount}; + auto assetAdj = ammAssetIn(balance, lptAMMBalance, tokens, tfee); + auto tokensAdj = tokens; + // Rounding didn't work the right way. + // Try to adjust the original deposit amount by difference + // in adjust and original amount. Then adjust tokens and deposit amount. + if (assetAdj > amount) + { + auto const adjAmount = amount - (assetAdj - amount); + auto const t = lpTokensOut(balance, adjAmount, lptAMMBalance, tfee); + tokensAdj = adjustLPTokens(lptAMMBalance, t, IsDeposit::Yes); + assetAdj = ammAssetIn(balance, lptAMMBalance, tokensAdj, tfee); + } + return {tokensAdj, std::min(amount, assetAdj)}; +} + +std::pair +adjustAssetOutByTokens( + Rules const& rules, + STAmount const& balance, + STAmount const& amount, + STAmount const& lptAMMBalance, + STAmount const& tokens, + std::uint16_t tfee) +{ + if (!rules.enabled(fixAMMv1_3)) + return {tokens, amount}; + auto assetAdj = ammAssetOut(balance, lptAMMBalance, tokens, tfee); + auto tokensAdj = tokens; + // Rounding didn't work the right way. + // Try to adjust the original deposit amount by difference + // in adjust and original amount. Then adjust tokens and deposit amount. + if (assetAdj > amount) + { + auto const adjAmount = amount - (assetAdj - amount); + auto const t = lpTokensIn(balance, adjAmount, lptAMMBalance, tfee); + tokensAdj = adjustLPTokens(lptAMMBalance, t, IsDeposit::No); + assetAdj = ammAssetOut(balance, lptAMMBalance, tokensAdj, tfee); + } + return {tokensAdj, std::min(amount, assetAdj)}; +} + +Number +adjustFracByTokens( + Rules const& rules, + STAmount const& lptAMMBalance, + STAmount const& tokens, + Number const& frac) +{ + if (!rules.enabled(fixAMMv1_3)) + return frac; + return tokens / lptAMMBalance; +} + } // namespace ripple diff --git a/src/xrpld/app/misc/detail/AMMUtils.cpp b/src/xrpld/app/misc/detail/AMMUtils.cpp index ba4c741300..b56ce2748e 100644 --- a/src/xrpld/app/misc/detail/AMMUtils.cpp +++ b/src/xrpld/app/misc/detail/AMMUtils.cpp @@ -17,6 +17,7 @@ */ //============================================================================== +#include #include #include @@ -464,4 +465,32 @@ isOnlyLiquidityProvider( return Unexpected(tecINTERNAL); // LCOV_EXCL_LINE } +Expected +verifyAndAdjustLPTokenBalance( + Sandbox& sb, + STAmount const& lpTokens, + std::shared_ptr& ammSle, + AccountID const& account) +{ + if (auto const res = isOnlyLiquidityProvider(sb, lpTokens.issue(), account); + !res) + return Unexpected(res.error()); + else if (res.value()) + { + if (withinRelativeDistance( + lpTokens, + ammSle->getFieldAmount(sfLPTokenBalance), + Number{1, -3})) + { + ammSle->setFieldAmount(sfLPTokenBalance, lpTokens); + sb.update(ammSle); + } + else + { + return Unexpected(tecAMM_INVALID_TOKENS); + } + } + return true; +} + } // namespace ripple diff --git a/src/xrpld/app/misc/detail/DelegateUtils.cpp b/src/xrpld/app/misc/detail/DelegateUtils.cpp index 7b7021fe9e..229af555ff 100644 --- a/src/xrpld/app/misc/detail/DelegateUtils.cpp +++ b/src/xrpld/app/misc/detail/DelegateUtils.cpp @@ -26,7 +26,7 @@ TER checkTxPermission(std::shared_ptr const& delegate, STTx const& tx) { if (!delegate) - return tecNO_PERMISSION; // LCOV_EXCL_LINE + return tecNO_DELEGATE_PERMISSION; // LCOV_EXCL_LINE auto const permissionArray = delegate->getFieldArray(sfPermissions); auto const txPermission = tx.getTxnType() + 1; @@ -38,7 +38,7 @@ checkTxPermission(std::shared_ptr const& delegate, STTx const& tx) return tesSUCCESS; } - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; } void diff --git a/src/xrpld/app/misc/detail/TxQ.cpp b/src/xrpld/app/misc/detail/TxQ.cpp index adf96d0e14..6924dae6c8 100644 --- a/src/xrpld/app/misc/detail/TxQ.cpp +++ b/src/xrpld/app/misc/detail/TxQ.cpp @@ -737,6 +737,13 @@ TxQ::apply( STAmountSO stAmountSO{view.rules().enabled(fixSTAmountCanonicalize)}; NumberSO stNumberSO{view.rules().enabled(fixUniversalNumber)}; + // See if the transaction is valid, properly formed, + // etc. before doing potentially expensive queue + // replace and multi-transaction operations. + auto const pfresult = preflight(app, view.rules(), *tx, flags, j); + if (pfresult.ter != tesSUCCESS) + return {pfresult.ter, false}; + // See if the transaction paid a high enough fee that it can go straight // into the ledger. if (auto directApplied = tryDirectApply(app, view, tx, flags, j)) @@ -749,13 +756,6 @@ TxQ::apply( // o The transaction paid a high enough fee that fee averaging will apply. // o The transaction will be queued. - // See if the transaction is valid, properly formed, - // etc. before doing potentially expensive queue - // replace and multi-transaction operations. - auto const pfresult = preflight(app, view.rules(), *tx, flags, j); - if (pfresult.ter != tesSUCCESS) - return {pfresult.ter, false}; - // If the account is not currently in the ledger, don't queue its tx. auto const account = (*tx)[sfAccount]; Keylet const accountKey{keylet::account(account)}; diff --git a/src/xrpld/app/paths/Flow.cpp b/src/xrpld/app/paths/Flow.cpp index 08f8ec3f25..3b14b8b968 100644 --- a/src/xrpld/app/paths/Flow.cpp +++ b/src/xrpld/app/paths/Flow.cpp @@ -64,6 +64,7 @@ flow( OfferCrossing offerCrossing, std::optional const& limitQuality, std::optional const& sendMax, + std::optional const& domainID, beast::Journal j, path::detail::FlowDebugInfo* flowDebugInfo) { @@ -98,6 +99,7 @@ flow( ownerPaysTransferFee, offerCrossing, ammContext, + domainID, j); if (toStrandsTer != tesSUCCESS) diff --git a/src/xrpld/app/paths/Flow.h b/src/xrpld/app/paths/Flow.h index 048b8785f1..659f180484 100644 --- a/src/xrpld/app/paths/Flow.h +++ b/src/xrpld/app/paths/Flow.h @@ -66,6 +66,7 @@ flow( OfferCrossing offerCrossing, std::optional const& limitQuality, std::optional const& sendMax, + std::optional const& domainID, beast::Journal j, path::detail::FlowDebugInfo* flowDebugInfo = nullptr); diff --git a/src/xrpld/app/paths/PathRequest.cpp b/src/xrpld/app/paths/PathRequest.cpp index ed090d25aa..8a88e774d0 100644 --- a/src/xrpld/app/paths/PathRequest.cpp +++ b/src/xrpld/app/paths/PathRequest.cpp @@ -438,6 +438,21 @@ PathRequest::parseJson(Json::Value const& jvParams) if (jvParams.isMember(jss::id)) jvId = jvParams[jss::id]; + if (jvParams.isMember(jss::domain)) + { + uint256 num; + if (!jvParams[jss::domain].isString() || + !num.parseHex(jvParams[jss::domain].asString())) + { + jvStatus = rpcError(rpcDOMAIN_MALFORMED); + return PFR_PJ_INVALID; + } + else + { + domain = num; + } + } + return PFR_PJ_NOCHANGE; } @@ -484,6 +499,7 @@ PathRequest::getPathFinder( std::nullopt, dst_amount, saSendMax, + domain, app_); if (pathfinder->findPaths(level, continueCallback)) pathfinder->computePathRanks(max_paths_, continueCallback); @@ -581,6 +597,7 @@ PathRequest::findPaths( *raDstAccount, // --> Account to deliver to. *raSrcAccount, // --> Account sending from. ps, // --> Path set. + domain, // --> Domain. app_.logs(), &rcInput); @@ -601,6 +618,7 @@ PathRequest::findPaths( *raDstAccount, // --> Account to deliver to. *raSrcAccount, // --> Account sending from. ps, // --> Path set. + domain, // --> Domain. app_.logs()); if (rc.result() != tesSUCCESS) diff --git a/src/xrpld/app/paths/PathRequest.h b/src/xrpld/app/paths/PathRequest.h index e480c2b812..aea0e564fb 100644 --- a/src/xrpld/app/paths/PathRequest.h +++ b/src/xrpld/app/paths/PathRequest.h @@ -25,6 +25,7 @@ #include #include +#include #include #include @@ -156,6 +157,8 @@ private: std::set sciSourceCurrencies; std::map mContext; + std::optional domain; + bool convert_all_; std::recursive_mutex mIndexLock; diff --git a/src/xrpld/app/paths/Pathfinder.cpp b/src/xrpld/app/paths/Pathfinder.cpp index e02c3ed089..74a33ec917 100644 --- a/src/xrpld/app/paths/Pathfinder.cpp +++ b/src/xrpld/app/paths/Pathfinder.cpp @@ -166,6 +166,7 @@ Pathfinder::Pathfinder( std::optional const& uSrcIssuer, STAmount const& saDstAmount, std::optional const& srcAmount, + std::optional const& domain, Application& app) : mSrcAccount(uSrcAccount) , mDstAccount(uDstAccount) @@ -184,6 +185,7 @@ Pathfinder::Pathfinder( 0, true))) , convert_all_(convertAllCheck(mDstAmount)) + , mDomain(domain) , mLedger(cache->getLedger()) , mRLCache(cache) , app_(app) @@ -372,6 +374,7 @@ Pathfinder::getPathLiquidity( mDstAccount, mSrcAccount, pathSet, + mDomain, app_.logs(), &rcInput); // If we can't get even the minimum liquidity requested, we're done. @@ -392,6 +395,7 @@ Pathfinder::getPathLiquidity( mDstAccount, mSrcAccount, pathSet, + mDomain, app_.logs(), &rcInput); @@ -431,6 +435,7 @@ Pathfinder::computePathRanks( mDstAccount, mSrcAccount, STPathSet(), + mDomain, app_.logs(), &rcInput); @@ -741,7 +746,7 @@ Pathfinder::getPathsOut( if (!bFrozen) { - count = app_.getOrderBookDB().getBookSize(issue); + count = app_.getOrderBookDB().getBookSize(issue, mDomain); if (auto const lines = mRLCache->getRippleLines(account, direction)) { @@ -1128,7 +1133,8 @@ Pathfinder::addLink( { // to XRP only if (!bOnXRP && - app_.getOrderBookDB().isBookToXRP({uEndCurrency, uEndIssuer})) + app_.getOrderBookDB().isBookToXRP( + {uEndCurrency, uEndIssuer}, mDomain)) { STPathElement pathElement( STPathElement::typeCurrency, @@ -1142,7 +1148,7 @@ Pathfinder::addLink( { bool bDestOnly = (addFlags & afOB_LAST) != 0; auto books = app_.getOrderBookDB().getBooksByTakerPays( - {uEndCurrency, uEndIssuer}); + {uEndCurrency, uEndIssuer}, mDomain); JLOG(j_.trace()) << books.size() << " books found from this currency/issuer"; diff --git a/src/xrpld/app/paths/Pathfinder.h b/src/xrpld/app/paths/Pathfinder.h index 973fda8855..ea3928dff4 100644 --- a/src/xrpld/app/paths/Pathfinder.h +++ b/src/xrpld/app/paths/Pathfinder.h @@ -48,6 +48,7 @@ public: std::optional const& uSrcIssuer, STAmount const& dstAmount, std::optional const& srcAmount, + std::optional const& domain, Application& app); Pathfinder(Pathfinder const&) = delete; Pathfinder& @@ -205,6 +206,7 @@ private: been removed. */ STAmount mRemainingAmount; bool convert_all_; + std::optional mDomain; std::shared_ptr mLedger; std::unique_ptr m_loadEvent; diff --git a/src/xrpld/app/paths/RippleCalc.cpp b/src/xrpld/app/paths/RippleCalc.cpp index c783bb8e9f..9c438bdfa9 100644 --- a/src/xrpld/app/paths/RippleCalc.cpp +++ b/src/xrpld/app/paths/RippleCalc.cpp @@ -53,6 +53,8 @@ RippleCalc::rippleCalculate( // A set of paths that are included in the transaction that we'll // explore for liquidity. STPathSet const& spsPaths, + + std::optional const& domainID, Logs& l, Input const* const pInputs) { @@ -93,9 +95,6 @@ RippleCalc::rippleCalculate( return std::nullopt; }(); - bool const ownerPaysTransferFee = - view.rules().enabled(featureOwnerPaysFee); - try { flowOut = flow( @@ -106,10 +105,11 @@ RippleCalc::rippleCalculate( spsPaths, defaultPaths, partialPayment, - ownerPaysTransferFee, + false, OfferCrossing::no, limitQuality, sendMax, + domainID, j, nullptr); } diff --git a/src/xrpld/app/paths/RippleCalc.h b/src/xrpld/app/paths/RippleCalc.h index 45f68725cc..09de7334e8 100644 --- a/src/xrpld/app/paths/RippleCalc.h +++ b/src/xrpld/app/paths/RippleCalc.h @@ -111,6 +111,8 @@ public: // A set of paths that are included in the transaction that we'll // explore for liquidity. STPathSet const& spsPaths, + + std::optional const& domainID, Logs& l, Input const* const pInputs = nullptr); diff --git a/src/xrpld/app/paths/RippleLineCache.h b/src/xrpld/app/paths/RippleLineCache.h index 5a3188c810..6196211a70 100644 --- a/src/xrpld/app/paths/RippleLineCache.h +++ b/src/xrpld/app/paths/RippleLineCache.h @@ -104,7 +104,7 @@ private: struct Hash { - explicit Hash() = default; + Hash() = default; std::size_t operator()(AccountKey const& key) const noexcept diff --git a/src/xrpld/app/paths/detail/BookStep.cpp b/src/xrpld/app/paths/detail/BookStep.cpp index 4024ca190d..554d2525f5 100644 --- a/src/xrpld/app/paths/detail/BookStep.cpp +++ b/src/xrpld/app/paths/detail/BookStep.cpp @@ -93,7 +93,7 @@ protected: public: BookStep(StrandContext const& ctx, Issue const& in, Issue const& out) : maxOffersToConsume_(getMaxOffersToConsume(ctx)) - , book_(in, out) + , book_(in, out, ctx.domainID) , strandSrc_(ctx.strandSrc) , strandDst_(ctx.strandDst) , prevStep_(ctx.prevStep) @@ -743,7 +743,6 @@ BookStep::forEachOffer( FlowOfferStream offers( sb, afView, book_, sb.parentCloseTime(), counter, j_); - bool const flowCross = afView.rules().enabled(featureFlowCross); bool offerAttempted = false; std::optional ofrQ; auto execOffer = [&](auto& offer) { @@ -760,8 +759,8 @@ BookStep::forEachOffer( // Make sure offer owner has authorization to own IOUs from issuer. // An account can always own XRP or their own IOUs. - if (flowCross && (!isXRP(offer.issueIn().currency)) && - (offer.owner() != offer.issueIn().account)) + if (!isXRP(offer.issueIn().currency) && + offer.owner() != offer.issueIn().account) { auto const& issuerID = offer.issueIn().account; auto const issuer = afView.read(keylet::account(issuerID)); @@ -837,6 +836,10 @@ BookStep::forEachOffer( // At any payment engine iteration, AMM offer can only be consumed once. auto tryAMM = [&](std::optional const& lobQuality) -> bool { + // amm doesn't support domain yet + if (book_.domain) + return true; + // If offer crossing then use either LOB quality or nullopt // to prevent AMM being blocked by a lower quality LOB. auto const qualityThreshold = [&]() -> std::optional { diff --git a/src/xrpld/app/paths/detail/PaySteps.cpp b/src/xrpld/app/paths/detail/PaySteps.cpp index 99f212d548..aa9e21e182 100644 --- a/src/xrpld/app/paths/detail/PaySteps.cpp +++ b/src/xrpld/app/paths/detail/PaySteps.cpp @@ -142,6 +142,7 @@ toStrand( bool ownerPaysTransferFee, OfferCrossing offerCrossing, AMMContext& ammContext, + std::optional const& domainID, beast::Journal j) { if (isXRP(src) || isXRP(dst) || !isConsistent(deliver) || @@ -279,6 +280,7 @@ toStrand( seenDirectIssues, seenBookOuts, ammContext, + domainID, j}; }; @@ -476,6 +478,7 @@ toStrands( bool ownerPaysTransferFee, OfferCrossing offerCrossing, AMMContext& ammContext, + std::optional const& domainID, beast::Journal j) { std::vector result; @@ -502,6 +505,7 @@ toStrands( ownerPaysTransferFee, offerCrossing, ammContext, + domainID, j); auto const ter = sp.first; auto& strand = sp.second; @@ -546,6 +550,7 @@ toStrands( ownerPaysTransferFee, offerCrossing, ammContext, + domainID, j); auto ter = sp.first; auto& strand = sp.second; @@ -592,6 +597,7 @@ StrandContext::StrandContext( std::array, 2>& seenDirectIssues_, boost::container::flat_set& seenBookOuts_, AMMContext& ammContext_, + std::optional const& domainID_, beast::Journal j_) : view(view_) , strandSrc(strandSrc_) @@ -608,6 +614,7 @@ StrandContext::StrandContext( , seenDirectIssues(seenDirectIssues_) , seenBookOuts(seenBookOuts_) , ammContext(ammContext_) + , domainID(domainID_) , j(j_) { } diff --git a/src/xrpld/app/paths/detail/Steps.h b/src/xrpld/app/paths/detail/Steps.h index bb9abf6545..0fcdc85fe1 100644 --- a/src/xrpld/app/paths/detail/Steps.h +++ b/src/xrpld/app/paths/detail/Steps.h @@ -23,6 +23,7 @@ #include #include +#include #include #include #include @@ -388,6 +389,7 @@ normalizePath( owner @param offerCrossing false -> payment; true -> offer crossing @param ammContext counts iterations with AMM offers + @param domainID the domain that order books will use @param j Journal for logging messages @return Error code and constructed Strand */ @@ -403,6 +405,7 @@ toStrand( bool ownerPaysTransferFee, OfferCrossing offerCrossing, AMMContext& ammContext, + std::optional const& domainID, beast::Journal j); /** @@ -427,6 +430,7 @@ toStrand( owner @param offerCrossing false -> payment; true -> offer crossing @param ammContext counts iterations with AMM offers + @param domainID the domain that order books will use @param j Journal for logging messages @return error code and collection of strands */ @@ -443,6 +447,7 @@ toStrands( bool ownerPaysTransferFee, OfferCrossing offerCrossing, AMMContext& ammContext, + std::optional const& domainID, beast::Journal j); /// @cond INTERNAL @@ -553,6 +558,7 @@ struct StrandContext */ boost::container::flat_set& seenBookOuts; AMMContext& ammContext; + std::optional domainID; // the domain the order book will use beast::Journal const j; /** StrandContext constructor. */ @@ -574,6 +580,7 @@ struct StrandContext boost::container::flat_set& seenBookOuts_, ///< For detecting book loops AMMContext& ammContext_, + std::optional const& domainID, beast::Journal j_); ///< Journal for logging }; diff --git a/src/xrpld/app/tx/applySteps.h b/src/xrpld/app/tx/applySteps.h index 2a5557ff4b..ec7180e263 100644 --- a/src/xrpld/app/tx/applySteps.h +++ b/src/xrpld/app/tx/applySteps.h @@ -165,6 +165,8 @@ struct PreflightResult public: /// From the input - the transaction STTx const& tx; + /// From the input - the batch identifier, if part of a batch + std::optional const parentBatchId; /// From the input - the rules Rules const rules; /// Consequences of the transaction @@ -183,6 +185,7 @@ public: Context const& ctx_, std::pair const& result) : tx(ctx_.tx) + , parentBatchId(ctx_.parentBatchId) , rules(ctx_.rules) , consequences(result.second) , flags(ctx_.flags) @@ -210,6 +213,8 @@ public: ReadView const& view; /// From the input - the transaction STTx const& tx; + /// From the input - the batch identifier, if part of a batch + std::optional const parentBatchId; /// From the input - the flags ApplyFlags const flags; /// From the input - the journal @@ -217,6 +222,7 @@ public: /// Intermediate transaction result TER const ter; + /// Success flag - whether the transaction is likely to /// claim a fee bool const likelyToClaimFee; @@ -226,6 +232,7 @@ public: PreclaimResult(Context const& ctx_, TER ter_) : view(ctx_.view) , tx(ctx_.tx) + , parentBatchId(ctx_.parentBatchId) , flags(ctx_.flags) , j(ctx_.j) , ter(ter_) @@ -255,6 +262,7 @@ public: @return A `PreflightResult` object containing, among other things, the `TER` code. */ +/** @{ */ PreflightResult preflight( Application& app, @@ -263,6 +271,16 @@ preflight( ApplyFlags flags, beast::Journal j); +PreflightResult +preflight( + Application& app, + Rules const& rules, + uint256 const& parentBatchId, + STTx const& tx, + ApplyFlags flags, + beast::Journal j); +/** @} */ + /** Gate a transaction based on static ledger information. The transaction is checked against all possible diff --git a/src/xrpld/app/tx/detail/AMMBid.cpp b/src/xrpld/app/tx/detail/AMMBid.cpp index 6fec46be90..86a80431b4 100644 --- a/src/xrpld/app/tx/detail/AMMBid.cpp +++ b/src/xrpld/app/tx/detail/AMMBid.cpp @@ -78,6 +78,21 @@ AMMBid::preflight(PreflightContext const& ctx) JLOG(ctx.j.debug()) << "AMM Bid: Invalid number of AuthAccounts."; return temMALFORMED; } + else if (ctx.rules.enabled(fixAMMv1_3)) + { + AccountID account = ctx.tx[sfAccount]; + std::set unique; + for (auto const& obj : authAccounts) + { + auto authAccount = obj[sfAccount]; + if (authAccount == account || unique.contains(authAccount)) + { + JLOG(ctx.j.debug()) << "AMM Bid: Invalid auth.account."; + return temMALFORMED; + } + unique.insert(authAccount); + } + } } return preflight2(ctx); @@ -232,7 +247,9 @@ applyBid( auctionSlot.makeFieldAbsent(sfAuthAccounts); // Burn the remaining bid amount auto const saBurn = adjustLPTokens( - lptAMMBalance, toSTAmount(lptAMMBalance.issue(), burn), false); + lptAMMBalance, + toSTAmount(lptAMMBalance.issue(), burn), + IsDeposit::No); if (saBurn >= lptAMMBalance) { // This error case should never occur. diff --git a/src/xrpld/app/tx/detail/AMMClawback.cpp b/src/xrpld/app/tx/detail/AMMClawback.cpp index 64a42374ec..07c5151727 100644 --- a/src/xrpld/app/tx/detail/AMMClawback.cpp +++ b/src/xrpld/app/tx/detail/AMMClawback.cpp @@ -151,6 +151,20 @@ AMMClawback::applyGuts(Sandbox& sb) if (!accountSle) return tecINTERNAL; // LCOV_EXCL_LINE + if (sb.rules().enabled(fixAMMClawbackRounding)) + { + // retrieve LP token balance inside the amendment gate to avoid + // inconsistent error behavior + auto const lpTokenBalance = ammLPHolds(sb, *ammSle, holder, j_); + if (lpTokenBalance == beast::zero) + return tecAMM_BALANCE; + + if (auto const res = verifyAndAdjustLPTokenBalance( + sb, lpTokenBalance, ammSle, holder); + !res) + return res.error(); // LCOV_EXCL_LINE + } + auto const expected = ammHolds( sb, *ammSle, @@ -248,10 +262,11 @@ AMMClawback::equalWithdrawMatchingOneAmount( STAmount const& amount) { auto frac = Number{amount} / amountBalance; - auto const amount2Withdraw = amount2Balance * frac; + auto amount2Withdraw = amount2Balance * frac; auto const lpTokensWithdraw = toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac); + if (lpTokensWithdraw > holdLPtokens) // if lptoken balance less than what the issuer intended to clawback, // clawback all the tokens. Because we are doing a two-asset withdrawal, @@ -272,6 +287,42 @@ AMMClawback::equalWithdrawMatchingOneAmount( mPriorBalance, ctx_.journal); + auto const& rules = sb.rules(); + if (rules.enabled(fixAMMClawbackRounding)) + { + auto tokensAdj = + getRoundedLPTokens(rules, lptAMMBalance, frac, IsDeposit::No); + + // LCOV_EXCL_START + if (tokensAdj == beast::zero) + return { + tecAMM_INVALID_TOKENS, STAmount{}, STAmount{}, std::nullopt}; + // LCOV_EXCL_STOP + + frac = adjustFracByTokens(rules, lptAMMBalance, tokensAdj, frac); + auto amount2Rounded = + getRoundedAsset(rules, amount2Balance, frac, IsDeposit::No); + + auto amountRounded = + getRoundedAsset(rules, amountBalance, frac, IsDeposit::No); + + return AMMWithdraw::withdraw( + sb, + ammSle, + ammAccount, + holder, + amountBalance, + amountRounded, + amount2Rounded, + lptAMMBalance, + tokensAdj, + 0, + FreezeHandling::fhIGNORE_FREEZE, + WithdrawAll::No, + mPriorBalance, + ctx_.journal); + } + // Because we are doing a two-asset withdrawal, // tfee is actually not used, so pass tfee as 0. return AMMWithdraw::withdraw( diff --git a/src/xrpld/app/tx/detail/AMMCreate.cpp b/src/xrpld/app/tx/detail/AMMCreate.cpp index 95cb5bf2e6..f0ccc6f298 100644 --- a/src/xrpld/app/tx/detail/AMMCreate.cpp +++ b/src/xrpld/app/tx/detail/AMMCreate.cpp @@ -329,7 +329,7 @@ applyCreate( << amount2; auto addOrderBook = [&](Issue const& issueIn, Issue const& issueOut, std::uint64_t uRate) { - Book const book{issueIn, issueOut}; + Book const book{issueIn, issueOut, std::nullopt}; auto const dir = keylet::quality(keylet::book(book), uRate); if (auto const bookExisted = static_cast(sb.read(dir)); !bookExisted) diff --git a/src/xrpld/app/tx/detail/AMMDeposit.cpp b/src/xrpld/app/tx/detail/AMMDeposit.cpp index 6a718a3f04..0dafa0da6c 100644 --- a/src/xrpld/app/tx/detail/AMMDeposit.cpp +++ b/src/xrpld/app/tx/detail/AMMDeposit.cpp @@ -542,7 +542,7 @@ AMMDeposit::deposit( lptAMMBalance, lpTokensDeposit, tfee, - true); + IsDeposit::Yes); if (lpTokensDepositActual <= beast::zero) { @@ -625,6 +625,17 @@ AMMDeposit::deposit( return {tesSUCCESS, lptAMMBalance + lpTokensDepositActual}; } +static STAmount +adjustLPTokensOut( + Rules const& rules, + STAmount const& lptAMMBalance, + STAmount const& lpTokensDeposit) +{ + if (!rules.enabled(fixAMMv1_3)) + return lpTokensDeposit; + return adjustLPTokens(lptAMMBalance, lpTokensDeposit, IsDeposit::Yes); +} + /** Proportional deposit of pools assets in exchange for the specified * amount of LPTokens. */ @@ -642,16 +653,25 @@ AMMDeposit::equalDepositTokens( { try { + auto const tokensAdj = + adjustLPTokensOut(view.rules(), lptAMMBalance, lpTokensDeposit); + if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}}; auto const frac = - divide(lpTokensDeposit, lptAMMBalance, lptAMMBalance.issue()); + divide(tokensAdj, lptAMMBalance, lptAMMBalance.issue()); + // amounts factor in the adjusted tokens + auto const amountDeposit = + getRoundedAsset(view.rules(), amountBalance, frac, IsDeposit::Yes); + auto const amount2Deposit = + getRoundedAsset(view.rules(), amount2Balance, frac, IsDeposit::Yes); return deposit( view, ammAccount, amountBalance, - multiply(amountBalance, frac, amountBalance.issue()), - multiply(amount2Balance, frac, amount2Balance.issue()), + amountDeposit, + amount2Deposit, lptAMMBalance, - lpTokensDeposit, + tokensAdj, depositMin, deposit2Min, std::nullopt, @@ -708,37 +728,55 @@ AMMDeposit::equalDepositLimit( std::uint16_t tfee) { auto frac = Number{amount} / amountBalance; - auto tokens = toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac); - if (tokens == beast::zero) - return {tecAMM_FAILED, STAmount{}}; - auto const amount2Deposit = amount2Balance * frac; + auto tokensAdj = + getRoundedLPTokens(view.rules(), lptAMMBalance, frac, IsDeposit::Yes); + if (tokensAdj == beast::zero) + { + if (!view.rules().enabled(fixAMMv1_3)) + return {tecAMM_FAILED, STAmount{}}; // LCOV_EXCL_LINE + else + return {tecAMM_INVALID_TOKENS, STAmount{}}; + } + // factor in the adjusted tokens + frac = adjustFracByTokens(view.rules(), lptAMMBalance, tokensAdj, frac); + auto const amount2Deposit = + getRoundedAsset(view.rules(), amount2Balance, frac, IsDeposit::Yes); if (amount2Deposit <= amount2) return deposit( view, ammAccount, amountBalance, amount, - toSTAmount(amount2Balance.issue(), amount2Deposit), + amount2Deposit, lptAMMBalance, - tokens, + tokensAdj, std::nullopt, std::nullopt, lpTokensDepositMin, tfee); frac = Number{amount2} / amount2Balance; - tokens = toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac); - if (tokens == beast::zero) - return {tecAMM_FAILED, STAmount{}}; - auto const amountDeposit = amountBalance * frac; + tokensAdj = + getRoundedLPTokens(view.rules(), lptAMMBalance, frac, IsDeposit::Yes); + if (tokensAdj == beast::zero) + { + if (!view.rules().enabled(fixAMMv1_3)) + return {tecAMM_FAILED, STAmount{}}; // LCOV_EXCL_LINE + else + return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE + } + // factor in the adjusted tokens + frac = adjustFracByTokens(view.rules(), lptAMMBalance, tokensAdj, frac); + auto const amountDeposit = + getRoundedAsset(view.rules(), amountBalance, frac, IsDeposit::Yes); if (amountDeposit <= amount) return deposit( view, ammAccount, amountBalance, - toSTAmount(amountBalance.issue(), amountDeposit), + amountDeposit, amount2, lptAMMBalance, - tokens, + tokensAdj, std::nullopt, std::nullopt, lpTokensDepositMin, @@ -764,17 +802,30 @@ AMMDeposit::singleDeposit( std::optional const& lpTokensDepositMin, std::uint16_t tfee) { - auto const tokens = lpTokensIn(amountBalance, amount, lptAMMBalance, tfee); + auto const tokens = adjustLPTokensOut( + view.rules(), + lptAMMBalance, + lpTokensOut(amountBalance, amount, lptAMMBalance, tfee)); if (tokens == beast::zero) - return {tecAMM_FAILED, STAmount{}}; + { + if (!view.rules().enabled(fixAMMv1_3)) + return {tecAMM_FAILED, STAmount{}}; // LCOV_EXCL_LINE + else + return {tecAMM_INVALID_TOKENS, STAmount{}}; + } + // factor in the adjusted tokens + auto const [tokensAdj, amountDepositAdj] = adjustAssetInByTokens( + view.rules(), amountBalance, amount, lptAMMBalance, tokens, tfee); + if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE return deposit( view, ammAccount, amountBalance, - amount, + amountDepositAdj, std::nullopt, lptAMMBalance, - tokens, + tokensAdj, std::nullopt, std::nullopt, lpTokensDepositMin, @@ -798,8 +849,13 @@ AMMDeposit::singleDepositTokens( STAmount const& lpTokensDeposit, std::uint16_t tfee) { + auto const tokensAdj = + adjustLPTokensOut(view.rules(), lptAMMBalance, lpTokensDeposit); + if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}}; + // the adjusted tokens are factored in auto const amountDeposit = - ammAssetIn(amountBalance, lptAMMBalance, lpTokensDeposit, tfee); + ammAssetIn(amountBalance, lptAMMBalance, tokensAdj, tfee); if (amountDeposit > amount) return {tecAMM_FAILED, STAmount{}}; return deposit( @@ -809,7 +865,7 @@ AMMDeposit::singleDepositTokens( amountDeposit, std::nullopt, lptAMMBalance, - lpTokensDeposit, + tokensAdj, std::nullopt, std::nullopt, std::nullopt, @@ -853,20 +909,32 @@ AMMDeposit::singleDepositEPrice( { if (amount != beast::zero) { - auto const tokens = - lpTokensIn(amountBalance, amount, lptAMMBalance, tfee); + auto const tokens = adjustLPTokensOut( + view.rules(), + lptAMMBalance, + lpTokensOut(amountBalance, amount, lptAMMBalance, tfee)); if (tokens <= beast::zero) - return {tecAMM_FAILED, STAmount{}}; - auto const ep = Number{amount} / tokens; + { + if (!view.rules().enabled(fixAMMv1_3)) + return {tecAMM_FAILED, STAmount{}}; // LCOV_EXCL_LINE + else + return {tecAMM_INVALID_TOKENS, STAmount{}}; + } + // factor in the adjusted tokens + auto const [tokensAdj, amountDepositAdj] = adjustAssetInByTokens( + view.rules(), amountBalance, amount, lptAMMBalance, tokens, tfee); + if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE + auto const ep = Number{amountDepositAdj} / tokensAdj; if (ep <= ePrice) return deposit( view, ammAccount, amountBalance, - amount, + amountDepositAdj, std::nullopt, lptAMMBalance, - tokens, + tokensAdj, std::nullopt, std::nullopt, std::nullopt, @@ -897,21 +965,37 @@ AMMDeposit::singleDepositEPrice( auto const a1 = c * c; auto const b1 = c * c * f2 * f2 + 2 * c - d * d; auto const c1 = 2 * c * f2 * f2 + 1 - 2 * d * f2; - auto const amountDeposit = toSTAmount( - amountBalance.issue(), - f1 * amountBalance * solveQuadraticEq(a1, b1, c1)); + auto amtNoRoundCb = [&] { + return f1 * amountBalance * solveQuadraticEq(a1, b1, c1); + }; + auto amtProdCb = [&] { return f1 * solveQuadraticEq(a1, b1, c1); }; + auto const amountDeposit = getRoundedAsset( + view.rules(), amtNoRoundCb, amountBalance, amtProdCb, IsDeposit::Yes); if (amountDeposit <= beast::zero) return {tecAMM_FAILED, STAmount{}}; - auto const tokens = - toSTAmount(lptAMMBalance.issue(), amountDeposit / ePrice); + auto tokNoRoundCb = [&] { return amountDeposit / ePrice; }; + auto tokProdCb = [&] { return amountDeposit / ePrice; }; + auto const tokens = getRoundedLPTokens( + view.rules(), tokNoRoundCb, lptAMMBalance, tokProdCb, IsDeposit::Yes); + // factor in the adjusted tokens + auto const [tokensAdj, amountDepositAdj] = adjustAssetInByTokens( + view.rules(), + amountBalance, + amountDeposit, + lptAMMBalance, + tokens, + tfee); + if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE + return deposit( view, ammAccount, amountBalance, - amountDeposit, + amountDepositAdj, std::nullopt, lptAMMBalance, - tokens, + tokensAdj, std::nullopt, std::nullopt, std::nullopt, diff --git a/src/xrpld/app/tx/detail/AMMWithdraw.cpp b/src/xrpld/app/tx/detail/AMMWithdraw.cpp index 586f453c6f..2ad1a19df5 100644 --- a/src/xrpld/app/tx/detail/AMMWithdraw.cpp +++ b/src/xrpld/app/tx/detail/AMMWithdraw.cpp @@ -311,24 +311,9 @@ AMMWithdraw::applyGuts(Sandbox& sb) if (sb.rules().enabled(fixAMMv1_1)) { if (auto const res = - isOnlyLiquidityProvider(sb, lpTokens.issue(), account_); + verifyAndAdjustLPTokenBalance(sb, lpTokens, ammSle, account_); !res) return {res.error(), false}; - else if (res.value()) - { - if (withinRelativeDistance( - lpTokens, - ammSle->getFieldAmount(sfLPTokenBalance), - Number{1, -3})) - { - ammSle->setFieldAmount(sfLPTokenBalance, lpTokens); - sb.update(ammSle); - } - else - { - return {tecAMM_INVALID_TOKENS, false}; - } - } } auto const tfee = getTradingFee(ctx_.view(), *ammSle, account_); @@ -522,7 +507,7 @@ AMMWithdraw::withdraw( lpTokensAMMBalance, lpTokensWithdraw, tfee, - false); + IsDeposit::No); return std::make_tuple( amountWithdraw, amount2Withdraw, lpTokensWithdraw); }(); @@ -683,6 +668,20 @@ AMMWithdraw::withdraw( amount2WithdrawActual); } +static STAmount +adjustLPTokensIn( + Rules const& rules, + STAmount const& lptAMMBalance, + STAmount const& lpTokensWithdraw, + WithdrawAll withdrawAll) +{ + if (!rules.enabled(fixAMMv1_3) || withdrawAll == WithdrawAll::Yes) + return lpTokensWithdraw; + return adjustLPTokens(lptAMMBalance, lpTokensWithdraw, IsDeposit::No); +} + +/** Proportional withdrawal of pool assets for the amount of LPTokens. + */ std::pair AMMWithdraw::equalWithdrawTokens( Sandbox& view, @@ -786,16 +785,22 @@ AMMWithdraw::equalWithdrawTokens( journal); } - auto const frac = divide(lpTokensWithdraw, lptAMMBalance, noIssue()); - auto const withdrawAmount = - multiply(amountBalance, frac, amountBalance.issue()); - auto const withdraw2Amount = - multiply(amount2Balance, frac, amount2Balance.issue()); + auto const tokensAdj = adjustLPTokensIn( + view.rules(), lptAMMBalance, lpTokensWithdraw, withdrawAll); + if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + return { + tecAMM_INVALID_TOKENS, STAmount{}, STAmount{}, std::nullopt}; + // the adjusted tokens are factored in + auto const frac = divide(tokensAdj, lptAMMBalance, noIssue()); + auto const amountWithdraw = + getRoundedAsset(view.rules(), amountBalance, frac, IsDeposit::No); + auto const amount2Withdraw = + getRoundedAsset(view.rules(), amount2Balance, frac, IsDeposit::No); // LP is making equal withdrawal by tokens but the requested amount // of LP tokens is likely too small and results in one-sided pool // withdrawal due to round off. Fail so the user withdraws // more tokens. - if (withdrawAmount == beast::zero || withdraw2Amount == beast::zero) + if (amountWithdraw == beast::zero || amount2Withdraw == beast::zero) return {tecAMM_FAILED, STAmount{}, STAmount{}, STAmount{}}; return withdraw( @@ -804,10 +809,10 @@ AMMWithdraw::equalWithdrawTokens( ammAccount, account, amountBalance, - withdrawAmount, - withdraw2Amount, + amountWithdraw, + amount2Withdraw, lptAMMBalance, - lpTokensWithdraw, + tokensAdj, tfee, freezeHanding, withdrawAll, @@ -862,7 +867,16 @@ AMMWithdraw::equalWithdrawLimit( std::uint16_t tfee) { auto frac = Number{amount} / amountBalance; - auto const amount2Withdraw = amount2Balance * frac; + auto amount2Withdraw = + getRoundedAsset(view.rules(), amount2Balance, frac, IsDeposit::No); + auto tokensAdj = + getRoundedLPTokens(view.rules(), lptAMMBalance, frac, IsDeposit::No); + if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}}; + // factor in the adjusted tokens + frac = adjustFracByTokens(view.rules(), lptAMMBalance, tokensAdj, frac); + amount2Withdraw = + getRoundedAsset(view.rules(), amount2Balance, frac, IsDeposit::No); if (amount2Withdraw <= amount2) { return withdraw( @@ -871,26 +885,42 @@ AMMWithdraw::equalWithdrawLimit( ammAccount, amountBalance, amount, - toSTAmount(amount2.issue(), amount2Withdraw), + amount2Withdraw, lptAMMBalance, - toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac), + tokensAdj, tfee); } frac = Number{amount2} / amount2Balance; - auto const amountWithdraw = amountBalance * frac; - XRPL_ASSERT( - amountWithdraw <= amount, - "ripple::AMMWithdraw::equalWithdrawLimit : maximum amountWithdraw"); + auto amountWithdraw = + getRoundedAsset(view.rules(), amountBalance, frac, IsDeposit::No); + tokensAdj = + getRoundedLPTokens(view.rules(), lptAMMBalance, frac, IsDeposit::No); + if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE + // factor in the adjusted tokens + frac = adjustFracByTokens(view.rules(), lptAMMBalance, tokensAdj, frac); + amountWithdraw = + getRoundedAsset(view.rules(), amountBalance, frac, IsDeposit::No); + if (!view.rules().enabled(fixAMMv1_3)) + { + // LCOV_EXCL_START + XRPL_ASSERT( + amountWithdraw <= amount, + "ripple::AMMWithdraw::equalWithdrawLimit : maximum amountWithdraw"); + // LCOV_EXCL_STOP + } + else if (amountWithdraw > amount) + return {tecAMM_FAILED, STAmount{}}; // LCOV_EXCL_LINE return withdraw( view, ammSle, ammAccount, amountBalance, - toSTAmount(amount.issue(), amountWithdraw), + amountWithdraw, amount2, lptAMMBalance, - toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac), + tokensAdj, tfee); } @@ -909,19 +939,32 @@ AMMWithdraw::singleWithdraw( STAmount const& amount, std::uint16_t tfee) { - auto const tokens = lpTokensOut(amountBalance, amount, lptAMMBalance, tfee); + auto const tokens = adjustLPTokensIn( + view.rules(), + lptAMMBalance, + lpTokensIn(amountBalance, amount, lptAMMBalance, tfee), + isWithdrawAll(ctx_.tx)); if (tokens == beast::zero) - return {tecAMM_FAILED, STAmount{}}; - + { + if (!view.rules().enabled(fixAMMv1_3)) + return {tecAMM_FAILED, STAmount{}}; // LCOV_EXCL_LINE + else + return {tecAMM_INVALID_TOKENS, STAmount{}}; + } + // factor in the adjusted tokens + auto const [tokensAdj, amountWithdrawAdj] = adjustAssetOutByTokens( + view.rules(), amountBalance, amount, lptAMMBalance, tokens, tfee); + if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE return withdraw( view, ammSle, ammAccount, amountBalance, - amount, + amountWithdrawAdj, std::nullopt, lptAMMBalance, - tokens, + tokensAdj, tfee); } @@ -946,8 +989,13 @@ AMMWithdraw::singleWithdrawTokens( STAmount const& lpTokensWithdraw, std::uint16_t tfee) { + auto const tokensAdj = adjustLPTokensIn( + view.rules(), lptAMMBalance, lpTokensWithdraw, isWithdrawAll(ctx_.tx)); + if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}}; + // the adjusted tokens are factored in auto const amountWithdraw = - withdrawByTokens(amountBalance, lptAMMBalance, lpTokensWithdraw, tfee); + ammAssetOut(amountBalance, lptAMMBalance, tokensAdj, tfee); if (amount == beast::zero || amountWithdraw >= amount) { return withdraw( @@ -958,7 +1006,7 @@ AMMWithdraw::singleWithdrawTokens( amountWithdraw, std::nullopt, lptAMMBalance, - lpTokensWithdraw, + tokensAdj, tfee); } @@ -1007,11 +1055,27 @@ AMMWithdraw::singleWithdrawEPrice( // t = T*(T + A*E*(f - 2))/(T*f - A*E) Number const ae = amountBalance * ePrice; auto const f = getFee(tfee); - auto const tokens = lptAMMBalance * (lptAMMBalance + ae * (f - 2)) / - (lptAMMBalance * f - ae); - if (tokens <= 0) - return {tecAMM_FAILED, STAmount{}}; - auto const amountWithdraw = toSTAmount(amount.issue(), tokens / ePrice); + auto tokNoRoundCb = [&] { + return lptAMMBalance * (lptAMMBalance + ae * (f - 2)) / + (lptAMMBalance * f - ae); + }; + auto tokProdCb = [&] { + return (lptAMMBalance + ae * (f - 2)) / (lptAMMBalance * f - ae); + }; + auto const tokensAdj = getRoundedLPTokens( + view.rules(), tokNoRoundCb, lptAMMBalance, tokProdCb, IsDeposit::No); + if (tokensAdj <= beast::zero) + { + if (!view.rules().enabled(fixAMMv1_3)) + return {tecAMM_FAILED, STAmount{}}; + else + return {tecAMM_INVALID_TOKENS, STAmount{}}; + } + auto amtNoRoundCb = [&] { return tokensAdj / ePrice; }; + auto amtProdCb = [&] { return tokensAdj / ePrice; }; + // the adjusted tokens are factored in + auto const amountWithdraw = getRoundedAsset( + view.rules(), amtNoRoundCb, amount, amtProdCb, IsDeposit::No); if (amount == beast::zero || amountWithdraw >= amount) { return withdraw( @@ -1022,7 +1086,7 @@ AMMWithdraw::singleWithdrawEPrice( amountWithdraw, std::nullopt, lptAMMBalance, - toSTAmount(lptAMMBalance.issue(), tokens), + tokensAdj, tfee); } diff --git a/src/xrpld/app/tx/detail/AMMWithdraw.h b/src/xrpld/app/tx/detail/AMMWithdraw.h index ae9328cb05..1de91fd787 100644 --- a/src/xrpld/app/tx/detail/AMMWithdraw.h +++ b/src/xrpld/app/tx/detail/AMMWithdraw.h @@ -301,7 +301,7 @@ private: std::uint16_t tfee); /** Check from the flags if it's withdraw all */ - WithdrawAll + static WithdrawAll isWithdrawAll(STTx const& tx); }; diff --git a/src/xrpld/app/tx/detail/ApplyContext.cpp b/src/xrpld/app/tx/detail/ApplyContext.cpp index 71fe246f15..79cbb7f40d 100644 --- a/src/xrpld/app/tx/detail/ApplyContext.cpp +++ b/src/xrpld/app/tx/detail/ApplyContext.cpp @@ -29,6 +29,7 @@ namespace ripple { ApplyContext::ApplyContext( Application& app_, OpenView& base, + std::optional const& parentBatchId, STTx const& tx_, TER preclaimResult_, XRPAmount baseFee_, @@ -41,7 +42,11 @@ ApplyContext::ApplyContext( , journal(journal_) , base_(base) , flags_(flags) + , parentBatchId_(parentBatchId) { + XRPL_ASSERT( + parentBatchId.has_value() == ((flags_ & tapBATCH) == tapBATCH), + "Parent Batch ID should be set if batch apply flag is set"); view_.emplace(&base_, flags_); } @@ -54,7 +59,8 @@ ApplyContext::discard() std::optional ApplyContext::apply(TER ter) { - return view_->apply(base_, tx, ter, flags_ & tapDRY_RUN, journal); + return view_->apply( + base_, tx, ter, parentBatchId_, flags_ & tapDRY_RUN, journal); } std::size_t diff --git a/src/xrpld/app/tx/detail/ApplyContext.h b/src/xrpld/app/tx/detail/ApplyContext.h index 715d4ea471..720d0aeea3 100644 --- a/src/xrpld/app/tx/detail/ApplyContext.h +++ b/src/xrpld/app/tx/detail/ApplyContext.h @@ -39,11 +39,34 @@ public: explicit ApplyContext( Application& app, OpenView& base, + std::optional const& parentBatchId, STTx const& tx, TER preclaimResult, XRPAmount baseFee, ApplyFlags flags, - beast::Journal = beast::Journal{beast::Journal::getNullSink()}); + beast::Journal journal = beast::Journal{beast::Journal::getNullSink()}); + + explicit ApplyContext( + Application& app, + OpenView& base, + STTx const& tx, + TER preclaimResult, + XRPAmount baseFee, + ApplyFlags flags, + beast::Journal journal = beast::Journal{beast::Journal::getNullSink()}) + : ApplyContext( + app, + base, + std::nullopt, + tx, + preclaimResult, + baseFee, + flags, + journal) + { + XRPL_ASSERT( + (flags & tapBATCH) == 0, "Batch apply flag should not be set"); + } Application& app; STTx const& tx; @@ -131,6 +154,9 @@ private: OpenView& base_; ApplyFlags flags_; std::optional view_; + + // The ID of the batch transaction we are executing under, if seated. + std::optional parentBatchId_; }; } // namespace ripple diff --git a/src/xrpld/app/tx/detail/Batch.cpp b/src/xrpld/app/tx/detail/Batch.cpp new file mode 100644 index 0000000000..40991ea99a --- /dev/null +++ b/src/xrpld/app/tx/detail/Batch.cpp @@ -0,0 +1,502 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace ripple { + +/** + * @brief Calculates the total base fee for a batch transaction. + * + * This function computes the required base fee for a batch transaction, + * including the base fee for the batch itself, the sum of base fees for + * all inner transactions, and additional fees for each batch signer. + * It performs overflow checks and validates the structure of the batch + * and its signers. + * + * @param view The ledger view providing fee and state information. + * @param tx The batch transaction to calculate the fee for. + * @return XRPAmount The total base fee required for the batch transaction. + * + * @throws std::overflow_error If any fee calculation would overflow the + * XRPAmount type. + * @throws std::length_error If the number of inner transactions or signers + * exceeds the allowed maximum. + * @throws std::invalid_argument If an inner transaction is itself a batch + * transaction. + */ +XRPAmount +Batch::calculateBaseFee(ReadView const& view, STTx const& tx) +{ + XRPAmount const maxAmount{ + std::numeric_limits::max()}; + + // batchBase: view.fees().base for batch processing + default base fee + XRPAmount const baseFee = Transactor::calculateBaseFee(view, tx); + + // LCOV_EXCL_START + if (baseFee > maxAmount - view.fees().base) + { + JLOG(debugLog().error()) << "BatchTrace: Base fee overflow detected."; + return XRPAmount{INITIAL_XRP}; + } + // LCOV_EXCL_STOP + + XRPAmount const batchBase = view.fees().base + baseFee; + + // Calculate the Inner Txn Fees + XRPAmount txnFees{0}; + if (tx.isFieldPresent(sfRawTransactions)) + { + auto const& txns = tx.getFieldArray(sfRawTransactions); + + // LCOV_EXCL_START + if (txns.size() > maxBatchTxCount) + { + JLOG(debugLog().error()) + << "BatchTrace: Raw Transactions array exceeds max entries."; + return XRPAmount{INITIAL_XRP}; + } + // LCOV_EXCL_STOP + + for (STObject txn : txns) + { + STTx const stx = STTx{std::move(txn)}; + + // LCOV_EXCL_START + if (stx.getTxnType() == ttBATCH) + { + JLOG(debugLog().error()) + << "BatchTrace: Inner Batch transaction found."; + return XRPAmount{INITIAL_XRP}; + } + // LCOV_EXCL_STOP + + auto const fee = ripple::calculateBaseFee(view, stx); + // LCOV_EXCL_START + if (txnFees > maxAmount - fee) + { + JLOG(debugLog().error()) + << "BatchTrace: XRPAmount overflow in txnFees calculation."; + return XRPAmount{INITIAL_XRP}; + } + // LCOV_EXCL_STOP + txnFees += fee; + } + } + + // Calculate the Signers/BatchSigners Fees + std::int32_t signerCount = 0; + if (tx.isFieldPresent(sfBatchSigners)) + { + auto const& signers = tx.getFieldArray(sfBatchSigners); + + // LCOV_EXCL_START + if (signers.size() > maxBatchTxCount) + { + JLOG(debugLog().error()) + << "BatchTrace: Batch Signers array exceeds max entries."; + return XRPAmount{INITIAL_XRP}; + } + // LCOV_EXCL_STOP + + for (STObject const& signer : signers) + { + if (signer.isFieldPresent(sfTxnSignature)) + signerCount += 1; + else if (signer.isFieldPresent(sfSigners)) + signerCount += signer.getFieldArray(sfSigners).size(); + } + } + + // LCOV_EXCL_START + if (signerCount > 0 && view.fees().base > maxAmount / signerCount) + { + JLOG(debugLog().error()) + << "BatchTrace: XRPAmount overflow in signerCount calculation."; + return XRPAmount{INITIAL_XRP}; + } + // LCOV_EXCL_STOP + + XRPAmount signerFees = signerCount * view.fees().base; + + // LCOV_EXCL_START + if (signerFees > maxAmount - txnFees) + { + JLOG(debugLog().error()) + << "BatchTrace: XRPAmount overflow in signerFees calculation."; + return XRPAmount{INITIAL_XRP}; + } + if (txnFees + signerFees > maxAmount - batchBase) + { + JLOG(debugLog().error()) + << "BatchTrace: XRPAmount overflow in total fee calculation."; + return XRPAmount{INITIAL_XRP}; + } + // LCOV_EXCL_STOP + + // 10 drops per batch signature + sum of inner tx fees + batchBase + return signerFees + txnFees + batchBase; +} + +/** + * @brief Performs preflight validation checks for a Batch transaction. + * + * This function validates the structure and contents of a Batch transaction + * before it is processed. It ensures that the Batch feature is enabled, + * checks for valid flags, validates the number and uniqueness of inner + * transactions, and enforces correct signing and fee requirements. + * + * The following validations are performed: + * - The Batch feature must be enabled in the current rules. + * - Only one of the mutually exclusive batch flags must be set. + * - The batch must contain at least two and no more than the maximum allowed + * inner transactions. + * - Each inner transaction must: + * - Be unique within the batch. + * - Not itself be a Batch transaction. + * - Have the tfInnerBatchTxn flag set. + * - Not include a TxnSignature or Signers field. + * - Have an empty SigningPubKey. + * - Pass its own preflight checks. + * - Have a fee of zero. + * - Have either Sequence or TicketSequence set, but not both or neither. + * - Not duplicate Sequence or TicketSequence values for the same account (for + * certain flags). + * - Validates that all required inner transaction accounts are present in the + * batch signers array, and that all batch signers are unique and not the outer + * account. + * - Verifies the batch signature if batch signers are present. + * + * @param ctx The PreflightContext containing the transaction and environment. + * @return NotTEC Returns tesSUCCESS if all checks pass, or an appropriate error + * code otherwise. + */ +NotTEC +Batch::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureBatch)) + return temDISABLED; + + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + auto const parentBatchId = ctx.tx.getTransactionID(); + auto const outerAccount = ctx.tx.getAccountID(sfAccount); + auto const flags = ctx.tx.getFlags(); + + if (flags & tfBatchMask) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]:" + << "invalid flags."; + return temINVALID_FLAG; + } + + if (std::popcount( + flags & + (tfAllOrNothing | tfOnlyOne | tfUntilFailure | tfIndependent)) != 1) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]:" + << "too many flags."; + return temINVALID_FLAG; + } + + auto const& rawTxns = ctx.tx.getFieldArray(sfRawTransactions); + if (rawTxns.size() <= 1) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]:" + << "txns array must have at least 2 entries."; + return temARRAY_EMPTY; + } + + if (rawTxns.size() > maxBatchTxCount) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]:" + << "txns array exceeds 8 entries."; + return temARRAY_TOO_LARGE; + } + + // Validation Inner Batch Txns + std::unordered_set requiredSigners; + std::unordered_set uniqueHashes; + std::unordered_map> + accountSeqTicket; + for (STObject rb : rawTxns) + { + STTx const stx = STTx{std::move(rb)}; + auto const hash = stx.getTransactionID(); + if (!uniqueHashes.emplace(hash).second) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " + << "duplicate Txn found. " + << "txID: " << hash; + return temREDUNDANT; + } + + if (stx.getFieldU16(sfTransactionType) == ttBATCH) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " + << "batch cannot have an inner batch txn. " + << "txID: " << hash; + return temINVALID; + } + + if (!(stx.getFlags() & tfInnerBatchTxn)) + { + JLOG(ctx.j.debug()) + << "BatchTrace[" << parentBatchId << "]: " + << "inner txn must have the tfInnerBatchTxn flag. " + << "txID: " << hash; + return temINVALID_FLAG; + } + + if (stx.isFieldPresent(sfTxnSignature)) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " + << "inner txn cannot include TxnSignature. " + << "txID: " << hash; + return temBAD_SIGNATURE; + } + + if (stx.isFieldPresent(sfSigners)) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " + << "inner txn cannot include Signers. " + << "txID: " << hash; + return temBAD_SIGNER; + } + + if (!stx.getSigningPubKey().empty()) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " + << "inner txn SigningPubKey must be empty. " + << "txID: " << hash; + return temBAD_REGKEY; + } + + auto const innerAccount = stx.getAccountID(sfAccount); + if (auto const preflightResult = ripple::preflight( + ctx.app, ctx.rules, parentBatchId, stx, tapBATCH, ctx.j); + preflightResult.ter != tesSUCCESS) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " + << "inner txn preflight failed: " + << transHuman(preflightResult.ter) << " " + << "txID: " << hash; + return temINVALID_INNER_BATCH; + } + + // Check that the fee is zero + if (auto const fee = stx.getFieldAmount(sfFee); + !fee.native() || fee.xrp() != beast::zero) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " + << "inner txn must have a fee of 0. " + << "txID: " << hash; + return temBAD_FEE; + } + + // Check that Sequence and TicketSequence are not both present + if (stx.isFieldPresent(sfTicketSequence) && + stx.getFieldU32(sfSequence) != 0) + { + JLOG(ctx.j.debug()) + << "BatchTrace[" << parentBatchId << "]: " + << "inner txn must have exactly one of Sequence and " + "TicketSequence. " + << "txID: " << hash; + return temSEQ_AND_TICKET; + } + + // Verify that either Sequence or TicketSequence is present + if (!stx.isFieldPresent(sfTicketSequence) && + stx.getFieldU32(sfSequence) == 0) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " + << "inner txn must have either Sequence or " + "TicketSequence. " + << "txID: " << hash; + return temSEQ_AND_TICKET; + } + + // Duplicate sequence and ticket checks + if (flags & (tfAllOrNothing | tfUntilFailure)) + { + if (auto const seq = stx.getFieldU32(sfSequence); seq != 0) + { + if (!accountSeqTicket[innerAccount].insert(seq).second) + { + JLOG(ctx.j.debug()) + << "BatchTrace[" << parentBatchId << "]: " + << "duplicate sequence found: " + << "txID: " << hash; + return temREDUNDANT; + } + } + + if (stx.isFieldPresent(sfTicketSequence)) + { + if (auto const ticket = stx.getFieldU32(sfTicketSequence); + !accountSeqTicket[innerAccount].insert(ticket).second) + { + JLOG(ctx.j.debug()) + << "BatchTrace[" << parentBatchId << "]: " + << "duplicate ticket found: " + << "txID: " << hash; + return temREDUNDANT; + } + } + } + + // If the inner account is the same as the outer account, do not add the + // inner account to the required signers set. + if (innerAccount != outerAccount) + requiredSigners.insert(innerAccount); + } + + // LCOV_EXCL_START + if (auto const ret = preflight2(ctx); !isTesSuccess(ret)) + return ret; + // LCOV_EXCL_STOP + + // Validation Batch Signers + std::unordered_set batchSigners; + if (ctx.tx.isFieldPresent(sfBatchSigners)) + { + STArray const& signers = ctx.tx.getFieldArray(sfBatchSigners); + + // Check that the batch signers array is not too large. + if (signers.size() > maxBatchTxCount) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " + << "signers array exceeds 8 entries."; + return temARRAY_TOO_LARGE; + } + + // Add batch signers to the set to ensure all signer accounts are + // unique. Meanwhile, remove signer accounts from the set of inner + // transaction accounts (`requiredSigners`). By the end of the loop, + // `requiredSigners` should be empty, indicating that all inner + // accounts are matched with signers. + for (auto const& signer : signers) + { + AccountID const signerAccount = signer.getAccountID(sfAccount); + if (signerAccount == outerAccount) + { + JLOG(ctx.j.debug()) + << "BatchTrace[" << parentBatchId << "]: " + << "signer cannot be the outer account: " << signerAccount; + return temBAD_SIGNER; + } + + if (!batchSigners.insert(signerAccount).second) + { + JLOG(ctx.j.debug()) + << "BatchTrace[" << parentBatchId << "]: " + << "duplicate signer found: " << signerAccount; + return temREDUNDANT; + } + + // Check that the batch signer is in the required signers set. + // Remove it if it does, as it can be crossed off the list. + if (requiredSigners.erase(signerAccount) == 0) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " + << "no account signature for inner txn."; + return temBAD_SIGNER; + } + } + + // Check the batch signers signatures. + auto const sigResult = ctx.tx.checkBatchSign( + STTx::RequireFullyCanonicalSig::yes, ctx.rules); + + if (!sigResult) + { + JLOG(ctx.j.debug()) + << "BatchTrace[" << parentBatchId << "]: " + << "invalid batch txn signature: " << sigResult.error(); + return temBAD_SIGNATURE; + } + } + + if (!requiredSigners.empty()) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " + << "invalid batch signers."; + return temBAD_SIGNER; + } + return tesSUCCESS; +} + +/** + * @brief Checks the validity of signatures for a batch transaction. + * + * This method first verifies the standard transaction signature by calling + * Transactor::checkSign. If the signature is not valid it returns the + * corresponding error code. + * + * Next, it verifies the batch-specific signature requirements by calling + * Transactor::checkBatchSign. If this check fails, it also returns the + * corresponding error code. + * + * If both checks succeed, the function returns tesSUCCESS. + * + * @param ctx The PreclaimContext containing transaction and environment data. + * @return NotTEC Returns tesSUCCESS if all signature checks pass, or an error + * code otherwise. + */ +NotTEC +Batch::checkSign(PreclaimContext const& ctx) +{ + if (auto ret = Transactor::checkSign(ctx); !isTesSuccess(ret)) + return ret; + + if (auto ret = Transactor::checkBatchSign(ctx); !isTesSuccess(ret)) + return ret; + + return tesSUCCESS; +} + +/** + * @brief Applies the outer batch transaction. + * + * This method is responsible for applying the outer batch transaction. + * The inner transactions within the batch are applied separately in the + * `applyBatchTransactions` method after the outer transaction is processed. + * + * @return TER Returns tesSUCCESS to indicate successful application of the + * outer batch transaction. + */ +TER +Batch::doApply() +{ + return tesSUCCESS; +} + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/Batch.h b/src/xrpld/app/tx/detail/Batch.h new file mode 100644 index 0000000000..211bce0589 --- /dev/null +++ b/src/xrpld/app/tx/detail/Batch.h @@ -0,0 +1,55 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_BATCH_H_INCLUDED +#define RIPPLE_TX_BATCH_H_INCLUDED + +#include +#include + +#include +#include + +namespace ripple { + +class Batch : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit Batch(ApplyContext& ctx) : Transactor(ctx) + { + } + + static XRPAmount + calculateBaseFee(ReadView const& view, STTx const& tx); + + static NotTEC + preflight(PreflightContext const& ctx); + + static NotTEC + checkSign(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +} // namespace ripple + +#endif diff --git a/src/xrpld/app/tx/detail/CashCheck.cpp b/src/xrpld/app/tx/detail/CashCheck.cpp index cccda83a68..0f1d08689c 100644 --- a/src/xrpld/app/tx/detail/CashCheck.cpp +++ b/src/xrpld/app/tx/detail/CashCheck.cpp @@ -451,6 +451,7 @@ CashCheck::doApply() OfferCrossing::no, std::nullopt, sleCheck->getFieldAmount(sfSendMax), + std::nullopt, // check does not support domain viewJ); if (result.result() != tesSUCCESS) diff --git a/src/xrpld/app/tx/detail/CreateOffer.cpp b/src/xrpld/app/tx/detail/CreateOffer.cpp index d9bd57ec3c..9543a4fcd9 100644 --- a/src/xrpld/app/tx/detail/CreateOffer.cpp +++ b/src/xrpld/app/tx/detail/CreateOffer.cpp @@ -18,16 +18,20 @@ //============================================================================== #include +#include #include #include #include +#include #include #include #include +#include +#include +#include namespace ripple { - TxConsequences CreateOffer::makeTxConsequences(PreflightContext const& ctx) { @@ -42,6 +46,10 @@ CreateOffer::makeTxConsequences(PreflightContext const& ctx) NotTEC CreateOffer::preflight(PreflightContext const& ctx) { + if (ctx.tx.isFieldPresent(sfDomainID) && + !ctx.rules.enabled(featurePermissionedDEX)) + return temDISABLED; + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; @@ -56,6 +64,12 @@ CreateOffer::preflight(PreflightContext const& ctx) return temINVALID_FLAG; } + if (!ctx.rules.enabled(featurePermissionedDEX) && tx.isFlag(tfHybrid)) + return temINVALID_FLAG; + + if (tx.isFlag(tfHybrid) && !tx.isFieldPresent(sfDomainID)) + return temINVALID_FLAG; + bool const bImmediateOrCancel(uTxFlags & tfImmediateOrCancel); bool const bFillOrKill(uTxFlags & tfFillOrKill); @@ -198,6 +212,15 @@ CreateOffer::preclaim(PreclaimContext const& ctx) return result; } + // if domain is specified, make sure that domain exists and the offer create + // is part of the domain + if (ctx.tx.isFieldPresent(sfDomainID)) + { + if (!permissioned_dex::accountInDomain( + ctx.view, id, ctx.tx[sfDomainID])) + return tecNO_PERMISSION; + } + return tesSUCCESS; } @@ -367,7 +390,7 @@ CreateOffer::bridged_cross( OfferStream offers_direct( view, view_cancel, - Book(taker.issue_in(), taker.issue_out()), + Book(taker.issue_in(), taker.issue_out(), std::nullopt), when, stepCounter_, j_); @@ -375,7 +398,7 @@ CreateOffer::bridged_cross( OfferStream offers_leg1( view, view_cancel, - Book(taker.issue_in(), xrpIssue()), + Book(taker.issue_in(), xrpIssue(), std::nullopt), when, stepCounter_, j_); @@ -383,7 +406,7 @@ CreateOffer::bridged_cross( OfferStream offers_leg2( view, view_cancel, - Book(xrpIssue(), taker.issue_out()), + Book(xrpIssue(), taker.issue_out(), std::nullopt), when, stepCounter_, j_); @@ -551,7 +574,7 @@ CreateOffer::direct_cross( OfferStream offers( view, view_cancel, - Book(taker.issue_in(), taker.issue_out()), + Book(taker.issue_in(), taker.issue_out(), std::nullopt), when, stepCounter_, j_); @@ -656,59 +679,12 @@ CreateOffer::step_account(OfferStream& stream, Taker const& taker) return false; } -// Fill as much of the offer as possible by consuming offers -// already on the books. Return the status and the amount of -// the offer to left unfilled. -std::pair -CreateOffer::takerCross( - Sandbox& sb, - Sandbox& sbCancel, - Amounts const& takerAmount) -{ - NetClock::time_point const when = sb.parentCloseTime(); - - beast::WrappedSink takerSink(j_, "Taker "); - - Taker taker( - cross_type_, - sb, - account_, - takerAmount, - ctx_.tx.getFlags(), - beast::Journal(takerSink)); - - // If the taker is unfunded before we begin crossing - // there's nothing to do - just return an error. - // - // We check this in preclaim, but when selling XRP - // charged fees can cause a user's available balance - // to go to 0 (by causing it to dip below the reserve) - // so we check this case again. - if (taker.unfunded()) - { - JLOG(j_.debug()) << "Not crossing: taker is unfunded."; - return {tecUNFUNDED_OFFER, takerAmount}; - } - - try - { - if (cross_type_ == CrossType::IouToIou) - return bridged_cross(taker, sb, sbCancel, when); - - return direct_cross(taker, sb, sbCancel, when); - } - catch (std::exception const& e) - { - JLOG(j_.error()) << "Exception during offer crossing: " << e.what(); - return {tecINTERNAL, taker.remaining_offer()}; - } -} - std::pair CreateOffer::flowCross( PaymentSandbox& psb, PaymentSandbox& psbCancel, - Amounts const& takerAmount) + Amounts const& takerAmount, + std::optional const& domainID) { try { @@ -805,6 +781,7 @@ CreateOffer::flowCross( offerCrossing, threshold, sendMax, + domainID, j_); // If stale offers were found remove them. @@ -907,23 +884,17 @@ CreateOffer::flowCross( } std::pair -CreateOffer::cross(Sandbox& sb, Sandbox& sbCancel, Amounts const& takerAmount) +CreateOffer::cross( + Sandbox& sb, + Sandbox& sbCancel, + Amounts const& takerAmount, + std::optional const& domainID) { - if (sb.rules().enabled(featureFlowCross)) - { - PaymentSandbox psbFlow{&sb}; - PaymentSandbox psbCancelFlow{&sbCancel}; - auto const ret = flowCross(psbFlow, psbCancelFlow, takerAmount); - psbFlow.apply(sb); - psbCancelFlow.apply(sbCancel); - return ret; - } - - Sandbox sbTaker{&sb}; - Sandbox sbCancelTaker{&sbCancel}; - auto const ret = takerCross(sbTaker, sbCancelTaker, takerAmount); - sbTaker.apply(sb); - sbCancelTaker.apply(sbCancel); + PaymentSandbox psbFlow{&sb}; + PaymentSandbox psbCancelFlow{&sbCancel}; + auto const ret = flowCross(psbFlow, psbCancelFlow, takerAmount, domainID); + psbFlow.apply(sb); + psbCancelFlow.apply(sbCancel); return ret; } @@ -950,6 +921,54 @@ CreateOffer::preCompute() return Transactor::preCompute(); } +TER +CreateOffer::applyHybrid( + Sandbox& sb, + std::shared_ptr sleOffer, + Keylet const& offerKey, + STAmount const& saTakerPays, + STAmount const& saTakerGets, + std::function)> const& setDir) +{ + if (!sleOffer->isFieldPresent(sfDomainID)) + return tecINTERNAL; // LCOV_EXCL_LINE + + // set hybrid flag + sleOffer->setFlag(lsfHybrid); + + // if offer is hybrid, need to also place into open offer dir + Book const book{saTakerPays.issue(), saTakerGets.issue(), std::nullopt}; + + auto dir = + keylet::quality(keylet::book(book), getRate(saTakerGets, saTakerPays)); + bool const bookExists = sb.exists(dir); + + auto const bookNode = sb.dirAppend(dir, offerKey, [&](SLE::ref sle) { + // don't set domainID on the directory object since this directory is + // for open book + setDir(sle, std::nullopt); + }); + + if (!bookNode) + { + JLOG(j_.debug()) + << "final result: failed to add hybrid offer to open book"; + return tecDIR_FULL; // LCOV_EXCL_LINE + } + + STArray bookArr(sfAdditionalBooks, 1); + auto bookInfo = STObject::makeInnerObject(sfBook); + bookInfo.setFieldH256(sfBookDirectory, dir.key); + bookInfo.setFieldU64(sfBookNode, *bookNode); + bookArr.push_back(std::move(bookInfo)); + + if (!bookExists) + ctx_.app.getOrderBookDB().addOrderBook(book); + + sleOffer->setFieldArray(sfAdditionalBooks, bookArr); + return tesSUCCESS; +} + std::pair CreateOffer::applyGuts(Sandbox& sb, Sandbox& sbCancel) { @@ -961,9 +980,11 @@ CreateOffer::applyGuts(Sandbox& sb, Sandbox& sbCancel) bool const bImmediateOrCancel(uTxFlags & tfImmediateOrCancel); bool const bFillOrKill(uTxFlags & tfFillOrKill); bool const bSell(uTxFlags & tfSell); + bool const bHybrid(uTxFlags & tfHybrid); auto saTakerPays = ctx_.tx[sfTakerPays]; auto saTakerGets = ctx_.tx[sfTakerGets]; + auto const domainID = ctx_.tx[~sfDomainID]; auto const cancelSequence = ctx_.tx[~sfOfferSequence]; @@ -1080,7 +1101,8 @@ CreateOffer::applyGuts(Sandbox& sb, Sandbox& sbCancel) stream << " out: " << format_amount(takerAmount.out); } - std::tie(result, place_offer) = cross(sb, sbCancel, takerAmount); + std::tie(result, place_offer) = + cross(sb, sbCancel, takerAmount, domainID); // We expect the implementation of cross to succeed // or give a tec. @@ -1222,21 +1244,39 @@ CreateOffer::applyGuts(Sandbox& sb, Sandbox& sbCancel) adjustOwnerCount(sb, sleCreator, 1, viewJ); JLOG(j_.trace()) << "adding to book: " << to_string(saTakerPays.issue()) - << " : " << to_string(saTakerGets.issue()); + << " : " << to_string(saTakerGets.issue()) + << (domainID ? (" : " + to_string(*domainID)) : ""); - Book const book{saTakerPays.issue(), saTakerGets.issue()}; + Book const book{saTakerPays.issue(), saTakerGets.issue(), domainID}; // Add offer to order book, using the original rate // before any crossing occured. + // + // Regular offer - BookDirectory points to open directory + // + // Domain offer (w/o hyrbid) - BookDirectory points to domain + // directory + // + // Hybrid domain offer - BookDirectory points to domain directory, + // and AdditionalBooks field stores one entry that points to the open + // directory auto dir = keylet::quality(keylet::book(book), uRate); bool const bookExisted = static_cast(sb.peek(dir)); - auto const bookNode = sb.dirAppend(dir, offer_index, [&](SLE::ref sle) { + auto setBookDir = [&](SLE::ref sle, + std::optional const& maybeDomain) { sle->setFieldH160(sfTakerPaysCurrency, saTakerPays.issue().currency); sle->setFieldH160(sfTakerPaysIssuer, saTakerPays.issue().account); sle->setFieldH160(sfTakerGetsCurrency, saTakerGets.issue().currency); sle->setFieldH160(sfTakerGetsIssuer, saTakerGets.issue().account); sle->setFieldU64(sfExchangeRate, uRate); + if (maybeDomain) + sle->setFieldH256(sfDomainID, *maybeDomain); + }; + + auto const bookNode = sb.dirAppend(dir, offer_index, [&](SLE::ref sle) { + // sets domainID on book directory if it's a domain offer + setBookDir(sle, domainID); }); if (!bookNode) @@ -1259,6 +1299,18 @@ CreateOffer::applyGuts(Sandbox& sb, Sandbox& sbCancel) sleOffer->setFlag(lsfPassive); if (bSell) sleOffer->setFlag(lsfSell); + if (domainID) + sleOffer->setFieldH256(sfDomainID, *domainID); + + // if it's a hybrid offer, set hybrid flag, and create an open dir + if (bHybrid) + { + auto const res = applyHybrid( + sb, sleOffer, offer_index, saTakerPays, saTakerGets, setBookDir); + if (res != tesSUCCESS) + return {res, true}; // LCOV_EXCL_LINE + } + sb.insert(sleOffer); if (!bookExisted) diff --git a/src/xrpld/app/tx/detail/CreateOffer.h b/src/xrpld/app/tx/detail/CreateOffer.h index 35808c78fe..f995f4a5d6 100644 --- a/src/xrpld/app/tx/detail/CreateOffer.h +++ b/src/xrpld/app/tx/detail/CreateOffer.h @@ -109,30 +109,37 @@ private: bool reachedOfferCrossingLimit(Taker const& taker) const; - // Fill offer as much as possible by consuming offers already on the books, - // and adjusting account balances accordingly. - // - // Charges fees on top to taker. - std::pair - takerCross(Sandbox& sb, Sandbox& sbCancel, Amounts const& takerAmount); - // Use the payment flow code to perform offer crossing. std::pair flowCross( PaymentSandbox& psb, PaymentSandbox& psbCancel, - Amounts const& takerAmount); + Amounts const& takerAmount, + std::optional const& domainID); // Temporary // This is a central location that invokes both versions of cross // so the results can be compared. Eventually this layer will be // removed once flowCross is determined to be stable. std::pair - cross(Sandbox& sb, Sandbox& sbCancel, Amounts const& takerAmount); + cross( + Sandbox& sb, + Sandbox& sbCancel, + Amounts const& takerAmount, + std::optional const& domainID); static std::string format_amount(STAmount const& amount); + TER + applyHybrid( + Sandbox& sb, + std::shared_ptr sleOffer, + Keylet const& offer_index, + STAmount const& saTakerPays, + STAmount const& saTakerGets, + std::function)> const& setDir); + private: // What kind of offer we are placing CrossType cross_type_; diff --git a/src/xrpld/app/tx/detail/DelegateSet.cpp b/src/xrpld/app/tx/detail/DelegateSet.cpp index d93ed6fa96..34e1c3afd3 100644 --- a/src/xrpld/app/tx/detail/DelegateSet.cpp +++ b/src/xrpld/app/tx/detail/DelegateSet.cpp @@ -63,7 +63,7 @@ DelegateSet::preclaim(PreclaimContext const& ctx) return terNO_ACCOUNT; // LCOV_EXCL_LINE if (!ctx.view.exists(keylet::account(ctx.tx[sfAuthorize]))) - return terNO_ACCOUNT; + return tecNO_TARGET; auto const& permissions = ctx.tx.getFieldArray(sfPermissions); for (auto const& permission : permissions) diff --git a/src/xrpld/app/tx/detail/DeleteAccount.cpp b/src/xrpld/app/tx/detail/DeleteAccount.cpp index a2a9769d43..f9760358e4 100644 --- a/src/xrpld/app/tx/detail/DeleteAccount.cpp +++ b/src/xrpld/app/tx/detail/DeleteAccount.cpp @@ -58,7 +58,8 @@ DeleteAccount::preflight(PreflightContext const& ctx) // An account cannot be deleted and give itself the resulting XRP. return temDST_IS_SRC; - if (auto const err = credentials::checkFields(ctx); !isTesSuccess(err)) + if (auto const err = credentials::checkFields(ctx.tx, ctx.j); + !isTesSuccess(err)) return err; return preflight2(ctx); @@ -241,7 +242,8 @@ DeleteAccount::preclaim(PreclaimContext const& ctx) return tecDST_TAG_NEEDED; // If credentials are provided - check them anyway - if (auto const err = credentials::valid(ctx, account); !isTesSuccess(err)) + if (auto const err = credentials::valid(ctx.tx, ctx.view, account, ctx.j); + !isTesSuccess(err)) return err; // if credentials then postpone auth check to doApply, to check for expired @@ -376,7 +378,8 @@ DeleteAccount::doApply() if (ctx_.view().rules().enabled(featureDepositAuth) && ctx_.tx.isFieldPresent(sfCredentialIDs)) { - if (auto err = verifyDepositPreauth(ctx_, account_, dstID, dst); + if (auto err = verifyDepositPreauth( + ctx_.tx, ctx_.view(), account_, dstID, dst, ctx_.journal); !isTesSuccess(err)) return err; } diff --git a/src/xrpld/app/tx/detail/Escrow.cpp b/src/xrpld/app/tx/detail/Escrow.cpp index 0b58957fcf..8f7005d55c 100644 --- a/src/xrpld/app/tx/detail/Escrow.cpp +++ b/src/xrpld/app/tx/detail/Escrow.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -29,6 +30,7 @@ #include #include #include +#include #include #include @@ -79,7 +81,41 @@ namespace ripple { TxConsequences EscrowCreate::makeTxConsequences(PreflightContext const& ctx) { - return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()}; + return TxConsequences{ + ctx.tx, isXRP(ctx.tx[sfAmount]) ? ctx.tx[sfAmount].xrp() : beast::zero}; +} + +template +static NotTEC +escrowCreatePreflightHelper(PreflightContext const& ctx); + +template <> +NotTEC +escrowCreatePreflightHelper(PreflightContext const& ctx) +{ + STAmount const amount = ctx.tx[sfAmount]; + if (amount.native() || amount <= beast::zero) + return temBAD_AMOUNT; + + if (badCurrency() == amount.getCurrency()) + return temBAD_CURRENCY; + + return tesSUCCESS; +} + +template <> +NotTEC +escrowCreatePreflightHelper(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureMPTokensV1)) + return temDISABLED; + + auto const amount = ctx.tx[sfAmount]; + if (amount.native() || amount.mpt() > MPTAmount{maxMPTokenAmount} || + amount <= beast::zero) + return temBAD_AMOUNT; + + return tesSUCCESS; } NotTEC @@ -91,11 +127,25 @@ EscrowCreate::preflight(PreflightContext const& ctx) if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; - if (!isXRP(ctx.tx[sfAmount])) - return temBAD_AMOUNT; + STAmount const amount{ctx.tx[sfAmount]}; + if (!isXRP(amount)) + { + if (!ctx.rules.enabled(featureTokenEscrow)) + return temBAD_AMOUNT; - if (ctx.tx[sfAmount] <= beast::zero) - return temBAD_AMOUNT; + if (auto const ret = std::visit( + [&](T const&) { + return escrowCreatePreflightHelper(ctx); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + } + else + { + if (amount <= beast::zero) + return temBAD_AMOUNT; + } // We must specify at least one timeout value if (!ctx.tx[~sfCancelAfter] && !ctx.tx[~sfFinishAfter]) @@ -142,10 +192,181 @@ EscrowCreate::preflight(PreflightContext const& ctx) return preflight2(ctx); } +template +static TER +escrowCreatePreclaimHelper( + PreclaimContext const& ctx, + AccountID const& account, + AccountID const& dest, + STAmount const& amount); + +template <> +TER +escrowCreatePreclaimHelper( + PreclaimContext const& ctx, + AccountID const& account, + AccountID const& dest, + STAmount const& amount) +{ + AccountID issuer = amount.getIssuer(); + // If the issuer is the same as the account, return tecNO_PERMISSION + if (issuer == account) + return tecNO_PERMISSION; + + // If the lsfAllowTrustLineLocking is not enabled, return tecNO_PERMISSION + auto const sleIssuer = ctx.view.read(keylet::account(issuer)); + if (!sleIssuer) + return tecNO_ISSUER; + if (!sleIssuer->isFlag(lsfAllowTrustLineLocking)) + return tecNO_PERMISSION; + + // If the account does not have a trustline to the issuer, return tecNO_LINE + auto const sleRippleState = + ctx.view.read(keylet::line(account, issuer, amount.getCurrency())); + if (!sleRippleState) + return tecNO_LINE; + + STAmount const balance = (*sleRippleState)[sfBalance]; + + // If balance is positive, issuer must have higher address than account + if (balance > beast::zero && issuer < account) + return tecNO_PERMISSION; // LCOV_EXCL_LINE + + // If balance is negative, issuer must have lower address than account + if (balance < beast::zero && issuer > account) + return tecNO_PERMISSION; // LCOV_EXCL_LINE + + // If the issuer has requireAuth set, check if the account is authorized + if (auto const ter = requireAuth(ctx.view, amount.issue(), account); + ter != tesSUCCESS) + return ter; + + // If the issuer has requireAuth set, check if the destination is authorized + if (auto const ter = requireAuth(ctx.view, amount.issue(), dest); + ter != tesSUCCESS) + return ter; + + // If the issuer has frozen the account, return tecFROZEN + if (isFrozen(ctx.view, account, amount.issue())) + return tecFROZEN; + + // If the issuer has frozen the destination, return tecFROZEN + if (isFrozen(ctx.view, dest, amount.issue())) + return tecFROZEN; + + STAmount const spendableAmount = accountHolds( + ctx.view, + account, + amount.getCurrency(), + issuer, + fhIGNORE_FREEZE, + ctx.j); + + // If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS + if (spendableAmount <= beast::zero) + return tecINSUFFICIENT_FUNDS; + + // If the spendable amount is less than the amount, return + // tecINSUFFICIENT_FUNDS + if (spendableAmount < amount) + return tecINSUFFICIENT_FUNDS; + + // If the amount is not addable to the balance, return tecPRECISION_LOSS + if (!canAdd(spendableAmount, amount)) + return tecPRECISION_LOSS; + + return tesSUCCESS; +} + +template <> +TER +escrowCreatePreclaimHelper( + PreclaimContext const& ctx, + AccountID const& account, + AccountID const& dest, + STAmount const& amount) +{ + AccountID issuer = amount.getIssuer(); + // If the issuer is the same as the account, return tecNO_PERMISSION + if (issuer == account) + return tecNO_PERMISSION; + + // If the mpt does not exist, return tecOBJECT_NOT_FOUND + auto const issuanceKey = + keylet::mptIssuance(amount.get().getMptID()); + auto const sleIssuance = ctx.view.read(issuanceKey); + if (!sleIssuance) + return tecOBJECT_NOT_FOUND; + + // If the lsfMPTCanEscrow is not enabled, return tecNO_PERMISSION + if (!sleIssuance->isFlag(lsfMPTCanEscrow)) + return tecNO_PERMISSION; + + // If the issuer is not the same as the issuer of the mpt, return + // tecNO_PERMISSION + if (sleIssuance->getAccountID(sfIssuer) != issuer) + return tecNO_PERMISSION; // LCOV_EXCL_LINE + + // If the account does not have the mpt, return tecOBJECT_NOT_FOUND + if (!ctx.view.exists(keylet::mptoken(issuanceKey.key, account))) + return tecOBJECT_NOT_FOUND; + + // If the issuer has requireAuth set, check if the account is + // authorized + auto const& mptIssue = amount.get(); + if (auto const ter = + requireAuth(ctx.view, mptIssue, account, MPTAuthType::WeakAuth); + ter != tesSUCCESS) + return ter; + + // If the issuer has requireAuth set, check if the destination is + // authorized + if (auto const ter = + requireAuth(ctx.view, mptIssue, dest, MPTAuthType::WeakAuth); + ter != tesSUCCESS) + return ter; + + // If the issuer has frozen the account, return tecLOCKED + if (isFrozen(ctx.view, account, mptIssue)) + return tecLOCKED; + + // If the issuer has frozen the destination, return tecLOCKED + if (isFrozen(ctx.view, dest, mptIssue)) + return tecLOCKED; + + // If the mpt cannot be transferred, return tecNO_AUTH + if (auto const ter = canTransfer(ctx.view, mptIssue, account, dest); + ter != tesSUCCESS) + return ter; + + STAmount const spendableAmount = accountHolds( + ctx.view, + account, + amount.get(), + fhIGNORE_FREEZE, + ahIGNORE_AUTH, + ctx.j); + + // If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS + if (spendableAmount <= beast::zero) + return tecINSUFFICIENT_FUNDS; + + // If the spendable amount is less than the amount, return + // tecINSUFFICIENT_FUNDS + if (spendableAmount < amount) + return tecINSUFFICIENT_FUNDS; + + return tesSUCCESS; +} + TER EscrowCreate::preclaim(PreclaimContext const& ctx) { - auto const sled = ctx.view.read(keylet::account(ctx.tx[sfDestination])); + STAmount const amount{ctx.tx[sfAmount]}; + AccountID const account{ctx.tx[sfAccount]}; + AccountID const dest{ctx.tx[sfDestination]}; + + auto const sled = ctx.view.read(keylet::account(dest)); if (!sled) return tecNO_DST; @@ -156,6 +377,77 @@ EscrowCreate::preclaim(PreclaimContext const& ctx) if (isPseudoAccount(sled)) return tecNO_PERMISSION; + if (!isXRP(amount)) + { + if (!ctx.view.rules().enabled(featureTokenEscrow)) + return temDISABLED; // LCOV_EXCL_LINE + + if (auto const ret = std::visit( + [&](T const&) { + return escrowCreatePreclaimHelper( + ctx, account, dest, amount); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + } + return tesSUCCESS; +} + +template +static TER +escrowLockApplyHelper( + ApplyView& view, + AccountID const& issuer, + AccountID const& sender, + STAmount const& amount, + beast::Journal journal); + +template <> +TER +escrowLockApplyHelper( + ApplyView& view, + AccountID const& issuer, + AccountID const& sender, + STAmount const& amount, + beast::Journal journal) +{ + // Defensive: Issuer cannot create an escrow + // LCOV_EXCL_START + if (issuer == sender) + return tecINTERNAL; + // LCOV_EXCL_STOP + + auto const ter = rippleCredit( + view, + sender, + issuer, + amount, + amount.holds() ? false : true, + journal); + if (ter != tesSUCCESS) + return ter; // LCOV_EXCL_LINE + return tesSUCCESS; +} + +template <> +TER +escrowLockApplyHelper( + ApplyView& view, + AccountID const& issuer, + AccountID const& sender, + STAmount const& amount, + beast::Journal journal) +{ + // Defensive: Issuer cannot create an escrow + // LCOV_EXCL_START + if (issuer == sender) + return tecINTERNAL; + // LCOV_EXCL_STOP + + auto const ter = rippleLockEscrowMPT(view, sender, amount, journal); + if (ter != tesSUCCESS) + return ter; // LCOV_EXCL_LINE return tesSUCCESS; } @@ -196,21 +488,23 @@ EscrowCreate::doApply() } } - auto const account = ctx_.tx[sfAccount]; - auto const sle = ctx_.view().peek(keylet::account(account)); + auto const sle = ctx_.view().peek(keylet::account(account_)); if (!sle) - return tefINTERNAL; + return tefINTERNAL; // LCOV_EXCL_LINE // Check reserve and funds availability + STAmount const amount{ctx_.tx[sfAmount]}; + + auto const reserve = + ctx_.view().fees().accountReserve((*sle)[sfOwnerCount] + 1); + + if (mSourceBalance < reserve) + return tecINSUFFICIENT_RESERVE; + + // Check reserve and funds availability + if (isXRP(amount)) { - auto const balance = STAmount((*sle)[sfBalance]).xrp(); - auto const reserve = - ctx_.view().fees().accountReserve((*sle)[sfOwnerCount] + 1); - - if (balance < reserve) - return tecINSUFFICIENT_RESERVE; - - if (balance < reserve + STAmount(ctx_.tx[sfAmount]).xrp()) + if (mSourceBalance < reserve + STAmount(amount).xrp()) return tecUNFUNDED; } @@ -233,10 +527,10 @@ EscrowCreate::doApply() // Create escrow in ledger. Note that we we use the value from the // sequence or ticket. For more explanation see comments in SeqProxy.h. - Keylet const escrowKeylet = keylet::escrow(account, ctx_.tx.getSeqValue()); + Keylet const escrowKeylet = keylet::escrow(account_, ctx_.tx.getSeqValue()); auto const slep = std::make_shared(escrowKeylet); - (*slep)[sfAmount] = ctx_.tx[sfAmount]; - (*slep)[sfAccount] = account; + (*slep)[sfAmount] = amount; + (*slep)[sfAccount] = account_; (*slep)[~sfCondition] = ctx_.tx[~sfCondition]; (*slep)[~sfSourceTag] = ctx_.tx[~sfSourceTag]; (*slep)[sfDestination] = ctx_.tx[sfDestination]; @@ -244,32 +538,69 @@ EscrowCreate::doApply() (*slep)[~sfFinishAfter] = ctx_.tx[~sfFinishAfter]; (*slep)[~sfDestinationTag] = ctx_.tx[~sfDestinationTag]; + if (ctx_.view().rules().enabled(featureTokenEscrow) && !isXRP(amount)) + { + auto const xferRate = transferRate(ctx_.view(), amount); + if (xferRate != parityRate) + (*slep)[sfTransferRate] = xferRate.value; + } + ctx_.view().insert(slep); // Add escrow to sender's owner directory { auto page = ctx_.view().dirInsert( - keylet::ownerDir(account), escrowKeylet, describeOwnerDir(account)); + keylet::ownerDir(account_), + escrowKeylet, + describeOwnerDir(account_)); if (!page) - return tecDIR_FULL; + return tecDIR_FULL; // LCOV_EXCL_LINE (*slep)[sfOwnerNode] = *page; } // If it's not a self-send, add escrow to recipient's owner directory. - if (auto const dest = ctx_.tx[sfDestination]; dest != ctx_.tx[sfAccount]) + AccountID const dest = ctx_.tx[sfDestination]; + if (dest != account_) { auto page = ctx_.view().dirInsert( keylet::ownerDir(dest), escrowKeylet, describeOwnerDir(dest)); if (!page) - return tecDIR_FULL; + return tecDIR_FULL; // LCOV_EXCL_LINE (*slep)[sfDestinationNode] = *page; } - // Deduct owner's balance, increment owner count - (*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount]; + // IOU escrow objects are added to the issuer's owner directory to help + // track the total locked balance. For MPT, this isn't necessary because the + // locked balance is already stored directly in the MPTokenIssuance object. + AccountID const issuer = amount.getIssuer(); + if (!isXRP(amount) && issuer != account_ && issuer != dest && + !amount.holds()) + { + auto page = ctx_.view().dirInsert( + keylet::ownerDir(issuer), escrowKeylet, describeOwnerDir(issuer)); + if (!page) + return tecDIR_FULL; // LCOV_EXCL_LINE + (*slep)[sfIssuerNode] = *page; + } + + // Deduct owner's balance + if (isXRP(amount)) + (*sle)[sfBalance] = (*sle)[sfBalance] - amount; + else + { + if (auto const ret = std::visit( + [&](T const&) { + return escrowLockApplyHelper( + ctx_.view(), issuer, account_, amount, j_); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; // LCOV_EXCL_LINE + } + + // increment owner count adjustOwnerCount(ctx_.view(), sle, 1, ctx_.journal); ctx_.view().update(sle); - return tesSUCCESS; } @@ -341,7 +672,8 @@ EscrowFinish::preflight(PreflightContext const& ctx) } } - if (auto const err = credentials::checkFields(ctx); !isTesSuccess(err)) + if (auto const err = credentials::checkFields(ctx.tx, ctx.j); + !isTesSuccess(err)) return err; return tesSUCCESS; @@ -360,26 +692,337 @@ EscrowFinish::calculateBaseFee(ReadView const& view, STTx const& tx) return Transactor::calculateBaseFee(view, tx) + extraFee; } +template +static TER +escrowFinishPreclaimHelper( + PreclaimContext const& ctx, + AccountID const& dest, + STAmount const& amount); + +template <> +TER +escrowFinishPreclaimHelper( + PreclaimContext const& ctx, + AccountID const& dest, + STAmount const& amount) +{ + AccountID issuer = amount.getIssuer(); + // If the issuer is the same as the account, return tesSUCCESS + if (issuer == dest) + return tesSUCCESS; + + // If the issuer has requireAuth set, check if the destination is authorized + if (auto const ter = requireAuth(ctx.view, amount.issue(), dest); + ter != tesSUCCESS) + return ter; + + // If the issuer has deep frozen the destination, return tecFROZEN + if (isDeepFrozen(ctx.view, dest, amount.getCurrency(), amount.getIssuer())) + return tecFROZEN; + + return tesSUCCESS; +} + +template <> +TER +escrowFinishPreclaimHelper( + PreclaimContext const& ctx, + AccountID const& dest, + STAmount const& amount) +{ + AccountID issuer = amount.getIssuer(); + // If the issuer is the same as the dest, return tesSUCCESS + if (issuer == dest) + return tesSUCCESS; + + // If the mpt does not exist, return tecOBJECT_NOT_FOUND + auto const issuanceKey = + keylet::mptIssuance(amount.get().getMptID()); + auto const sleIssuance = ctx.view.read(issuanceKey); + if (!sleIssuance) + return tecOBJECT_NOT_FOUND; + + // If the issuer has requireAuth set, check if the destination is + // authorized + auto const& mptIssue = amount.get(); + if (auto const ter = + requireAuth(ctx.view, mptIssue, dest, MPTAuthType::WeakAuth); + ter != tesSUCCESS) + return ter; + + // If the issuer has frozen the destination, return tecLOCKED + if (isFrozen(ctx.view, dest, mptIssue)) + return tecLOCKED; + + return tesSUCCESS; +} + TER EscrowFinish::preclaim(PreclaimContext const& ctx) { - if (!ctx.view.rules().enabled(featureCredentials)) - return Transactor::preclaim(ctx); + if (ctx.view.rules().enabled(featureCredentials)) + { + if (auto const err = + credentials::valid(ctx.tx, ctx.view, ctx.tx[sfAccount], ctx.j); + !isTesSuccess(err)) + return err; + } - if (auto const err = credentials::valid(ctx, ctx.tx[sfAccount]); - !isTesSuccess(err)) - return err; + if (ctx.view.rules().enabled(featureTokenEscrow)) + { + auto const k = keylet::escrow(ctx.tx[sfOwner], ctx.tx[sfOfferSequence]); + auto const slep = ctx.view.read(k); + if (!slep) + return tecNO_TARGET; + AccountID const dest = (*slep)[sfDestination]; + STAmount const amount = (*slep)[sfAmount]; + + if (!isXRP(amount)) + { + if (auto const ret = std::visit( + [&](T const&) { + return escrowFinishPreclaimHelper(ctx, dest, amount); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + } + } return tesSUCCESS; } +template +static TER +escrowUnlockApplyHelper( + ApplyView& view, + Rate lockedRate, + std::shared_ptr const& sleDest, + STAmount const& xrpBalance, + STAmount const& amount, + AccountID const& issuer, + AccountID const& sender, + AccountID const& receiver, + bool createAsset, + beast::Journal journal); + +template <> +TER +escrowUnlockApplyHelper( + ApplyView& view, + Rate lockedRate, + std::shared_ptr const& sleDest, + STAmount const& xrpBalance, + STAmount const& amount, + AccountID const& issuer, + AccountID const& sender, + AccountID const& receiver, + bool createAsset, + beast::Journal journal) +{ + Keylet const trustLineKey = keylet::line(receiver, amount.issue()); + bool const recvLow = issuer > receiver; + bool const senderIssuer = issuer == sender; + bool const receiverIssuer = issuer == receiver; + bool const issuerHigh = issuer > receiver; + + // LCOV_EXCL_START + if (senderIssuer) + return tecINTERNAL; + // LCOV_EXCL_STOP + + if (receiverIssuer) + return tesSUCCESS; + + if (!view.exists(trustLineKey) && createAsset && !receiverIssuer) + { + // Can the account cover the trust line's reserve? + if (std::uint32_t const ownerCount = {sleDest->at(sfOwnerCount)}; + xrpBalance < view.fees().accountReserve(ownerCount + 1)) + { + JLOG(journal.trace()) << "Trust line does not exist. " + "Insufficent reserve to create line."; + + return tecNO_LINE_INSUF_RESERVE; + } + + Currency const currency = amount.getCurrency(); + STAmount initialBalance(amount.issue()); + initialBalance.setIssuer(noAccount()); + + // clang-format off + if (TER const ter = trustCreate( + view, // payment sandbox + recvLow, // is dest low? + issuer, // source + receiver, // destination + trustLineKey.key, // ledger index + sleDest, // Account to add to + false, // authorize account + (sleDest->getFlags() & lsfDefaultRipple) == 0, + false, // freeze trust line + false, // deep freeze trust line + initialBalance, // zero initial balance + Issue(currency, receiver), // limit of zero + 0, // quality in + 0, // quality out + journal); // journal + !isTesSuccess(ter)) + { + return ter; // LCOV_EXCL_LINE + } + // clang-format on + + view.update(sleDest); + } + + if (!view.exists(trustLineKey) && !receiverIssuer) + return tecNO_LINE; + + auto const xferRate = transferRate(view, amount); + // update if issuer rate is less than locked rate + if (xferRate < lockedRate) + lockedRate = xferRate; + + // Transfer Rate only applies when: + // 1. Issuer is not involved in the transfer (senderIssuer or + // receiverIssuer) + // 2. The locked rate is different from the parity rate + + // NOTE: Transfer fee in escrow works a bit differently from a normal + // payment. In escrow, the fee is deducted from the locked/sending amount, + // whereas in a normal payment, the transfer fee is taken on top of the + // sending amount. + auto finalAmt = amount; + if ((!senderIssuer && !receiverIssuer) && lockedRate != parityRate) + { + // compute transfer fee, if any + auto const xferFee = amount.value() - + divideRound(amount, lockedRate, amount.issue(), true); + // compute balance to transfer + finalAmt = amount.value() - xferFee; + } + + // validate the line limit if the account submitting txn is not the receiver + // of the funds + if (!createAsset) + { + auto const sleRippleState = view.peek(trustLineKey); + if (!sleRippleState) + return tecINTERNAL; // LCOV_EXCL_LINE + + // if the issuer is the high, then we use the low limit + // otherwise we use the high limit + STAmount const lineLimit = sleRippleState->getFieldAmount( + issuerHigh ? sfLowLimit : sfHighLimit); + + STAmount lineBalance = sleRippleState->getFieldAmount(sfBalance); + + // flip the sign of the line balance if the issuer is not high + if (!issuerHigh) + lineBalance.negate(); + + // add the final amount to the line balance + lineBalance += finalAmt; + + // if the transfer would exceed the line limit return tecLIMIT_EXCEEDED + if (lineLimit < lineBalance) + return tecLIMIT_EXCEEDED; + } + + // if destination is not the issuer then transfer funds + if (!receiverIssuer) + { + auto const ter = + rippleCredit(view, issuer, receiver, finalAmt, true, journal); + if (ter != tesSUCCESS) + return ter; // LCOV_EXCL_LINE + } + return tesSUCCESS; +} + +template <> +TER +escrowUnlockApplyHelper( + ApplyView& view, + Rate lockedRate, + std::shared_ptr const& sleDest, + STAmount const& xrpBalance, + STAmount const& amount, + AccountID const& issuer, + AccountID const& sender, + AccountID const& receiver, + bool createAsset, + beast::Journal journal) +{ + bool const senderIssuer = issuer == sender; + bool const receiverIssuer = issuer == receiver; + + auto const mptID = amount.get().getMptID(); + auto const issuanceKey = keylet::mptIssuance(mptID); + if (!view.exists(keylet::mptoken(issuanceKey.key, receiver)) && + createAsset && !receiverIssuer) + { + if (std::uint32_t const ownerCount = {sleDest->at(sfOwnerCount)}; + xrpBalance < view.fees().accountReserve(ownerCount + 1)) + { + return tecINSUFFICIENT_RESERVE; + } + + if (auto const ter = + MPTokenAuthorize::createMPToken(view, mptID, receiver, 0); + !isTesSuccess(ter)) + { + return ter; // LCOV_EXCL_LINE + } + + // update owner count. + adjustOwnerCount(view, sleDest, 1, journal); + } + + if (!view.exists(keylet::mptoken(issuanceKey.key, receiver)) && + !receiverIssuer) + return tecNO_PERMISSION; + + auto const xferRate = transferRate(view, amount); + // update if issuer rate is less than locked rate + if (xferRate < lockedRate) + lockedRate = xferRate; + + // Transfer Rate only applies when: + // 1. Issuer is not involved in the transfer (senderIssuer or + // receiverIssuer) + // 2. The locked rate is different from the parity rate + + // NOTE: Transfer fee in escrow works a bit differently from a normal + // payment. In escrow, the fee is deducted from the locked/sending amount, + // whereas in a normal payment, the transfer fee is taken on top of the + // sending amount. + auto finalAmt = amount; + if ((!senderIssuer && !receiverIssuer) && lockedRate != parityRate) + { + // compute transfer fee, if any + auto const xferFee = amount.value() - + divideRound(amount, lockedRate, amount.asset(), true); + // compute balance to transfer + finalAmt = amount.value() - xferFee; + } + + return rippleUnlockEscrowMPT(view, sender, receiver, finalAmt, journal); +} + TER EscrowFinish::doApply() { auto const k = keylet::escrow(ctx_.tx[sfOwner], ctx_.tx[sfOfferSequence]); auto const slep = ctx_.view().peek(k); if (!slep) + { + if (ctx_.view().rules().enabled(featureTokenEscrow)) + return tecINTERNAL; // LCOV_EXCL_LINE + return tecNO_TARGET; + } // If a cancel time is present, a finish operation should only succeed prior // to that time. fix1571 corrects a logic error in the check that would make @@ -466,7 +1109,8 @@ EscrowFinish::doApply() if (ctx_.view().rules().enabled(featureDepositAuth)) { - if (auto err = verifyDepositPreauth(ctx_, account_, destID, sled); + if (auto err = verifyDepositPreauth( + ctx_.tx, ctx_.view(), account_, destID, sled, ctx_.journal); !isTesSuccess(err)) return err; } @@ -495,8 +1139,50 @@ EscrowFinish::doApply() } } + STAmount const amount = slep->getFieldAmount(sfAmount); // Transfer amount to destination - (*sled)[sfBalance] = (*sled)[sfBalance] + (*slep)[sfAmount]; + if (isXRP(amount)) + (*sled)[sfBalance] = (*sled)[sfBalance] + amount; + else + { + if (!ctx_.view().rules().enabled(featureTokenEscrow)) + return temDISABLED; // LCOV_EXCL_LINE + + Rate lockedRate = slep->isFieldPresent(sfTransferRate) + ? ripple::Rate(slep->getFieldU32(sfTransferRate)) + : parityRate; + auto const issuer = amount.getIssuer(); + bool const createAsset = destID == account_; + if (auto const ret = std::visit( + [&](T const&) { + return escrowUnlockApplyHelper( + ctx_.view(), + lockedRate, + sled, + mPriorBalance, + amount, + issuer, + account, + destID, + createAsset, + j_); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + + // Remove escrow from issuers owner directory, if present. + if (auto const optPage = (*slep)[~sfIssuerNode]; optPage) + { + if (!ctx_.view().dirRemove( + keylet::ownerDir(issuer), *optPage, k.key, true)) + { + JLOG(j_.fatal()) << "Unable to delete Escrow from recipient."; + return tefBAD_LEDGER; // LCOV_EXCL_LINE + } + } + } + ctx_.view().update(sled); // Adjust source owner count @@ -506,7 +1192,6 @@ EscrowFinish::doApply() // Remove escrow from ledger ctx_.view().erase(slep); - return tesSUCCESS; } @@ -524,13 +1209,103 @@ EscrowCancel::preflight(PreflightContext const& ctx) return preflight2(ctx); } +template +static TER +escrowCancelPreclaimHelper( + PreclaimContext const& ctx, + AccountID const& account, + STAmount const& amount); + +template <> +TER +escrowCancelPreclaimHelper( + PreclaimContext const& ctx, + AccountID const& account, + STAmount const& amount) +{ + AccountID issuer = amount.getIssuer(); + // If the issuer is the same as the account, return tecINTERNAL + if (issuer == account) + return tecINTERNAL; // LCOV_EXCL_LINE + + // If the issuer has requireAuth set, check if the account is authorized + if (auto const ter = requireAuth(ctx.view, amount.issue(), account); + ter != tesSUCCESS) + return ter; + + return tesSUCCESS; +} + +template <> +TER +escrowCancelPreclaimHelper( + PreclaimContext const& ctx, + AccountID const& account, + STAmount const& amount) +{ + AccountID issuer = amount.getIssuer(); + // If the issuer is the same as the account, return tecINTERNAL + if (issuer == account) + return tecINTERNAL; // LCOV_EXCL_LINE + + // If the mpt does not exist, return tecOBJECT_NOT_FOUND + auto const issuanceKey = + keylet::mptIssuance(amount.get().getMptID()); + auto const sleIssuance = ctx.view.read(issuanceKey); + if (!sleIssuance) + return tecOBJECT_NOT_FOUND; + + // If the issuer has requireAuth set, check if the account is + // authorized + auto const& mptIssue = amount.get(); + if (auto const ter = + requireAuth(ctx.view, mptIssue, account, MPTAuthType::WeakAuth); + ter != tesSUCCESS) + return ter; + + return tesSUCCESS; +} + +TER +EscrowCancel::preclaim(PreclaimContext const& ctx) +{ + if (ctx.view.rules().enabled(featureTokenEscrow)) + { + auto const k = keylet::escrow(ctx.tx[sfOwner], ctx.tx[sfOfferSequence]); + auto const slep = ctx.view.read(k); + if (!slep) + return tecNO_TARGET; + + AccountID const account = (*slep)[sfAccount]; + STAmount const amount = (*slep)[sfAmount]; + + if (!isXRP(amount)) + { + if (auto const ret = std::visit( + [&](T const&) { + return escrowCancelPreclaimHelper( + ctx, account, amount); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + } + } + return tesSUCCESS; +} + TER EscrowCancel::doApply() { auto const k = keylet::escrow(ctx_.tx[sfOwner], ctx_.tx[sfOfferSequence]); auto const slep = ctx_.view().peek(k); if (!slep) + { + if (ctx_.view().rules().enabled(featureTokenEscrow)) + return tecINTERNAL; // LCOV_EXCL_LINE + return tecNO_TARGET; + } if (ctx_.view().rules().enabled(fix1571)) { @@ -580,9 +1355,49 @@ EscrowCancel::doApply() } } - // Transfer amount back to owner, decrement owner count auto const sle = ctx_.view().peek(keylet::account(account)); - (*sle)[sfBalance] = (*sle)[sfBalance] + (*slep)[sfAmount]; + STAmount const amount = slep->getFieldAmount(sfAmount); + + // Transfer amount back to the owner + if (isXRP(amount)) + (*sle)[sfBalance] = (*sle)[sfBalance] + amount; + else + { + if (!ctx_.view().rules().enabled(featureTokenEscrow)) + return temDISABLED; // LCOV_EXCL_LINE + + auto const issuer = amount.getIssuer(); + bool const createAsset = account == account_; + if (auto const ret = std::visit( + [&](T const&) { + return escrowUnlockApplyHelper( + ctx_.view(), + parityRate, + slep, + mPriorBalance, + amount, + issuer, + account, // sender and receiver are the same + account, + createAsset, + j_); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; // LCOV_EXCL_LINE + + // Remove escrow from issuers owner directory, if present. + if (auto const optPage = (*slep)[~sfIssuerNode]; optPage) + { + if (!ctx_.view().dirRemove( + keylet::ownerDir(issuer), *optPage, k.key, true)) + { + JLOG(j_.fatal()) << "Unable to delete Escrow from recipient."; + return tefBAD_LEDGER; // LCOV_EXCL_LINE + } + } + } + adjustOwnerCount(ctx_.view(), sle, -1, ctx_.journal); ctx_.view().update(sle); diff --git a/src/xrpld/app/tx/detail/Escrow.h b/src/xrpld/app/tx/detail/Escrow.h index 78acdbee00..2225c94f16 100644 --- a/src/xrpld/app/tx/detail/Escrow.h +++ b/src/xrpld/app/tx/detail/Escrow.h @@ -84,6 +84,9 @@ public: static NotTEC preflight(PreflightContext const& ctx); + static TER + preclaim(PreclaimContext const& ctx); + TER doApply() override; }; diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index 1a707e8496..9fbec47f7c 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -17,6 +17,8 @@ */ //============================================================================== +#include +#include #include #include #include @@ -160,7 +162,8 @@ XRPNotCreated::visitEntry( ((*before)[sfAmount] - (*before)[sfBalance]).xrp().drops(); break; case ltESCROW: - drops_ -= (*before)[sfAmount].xrp().drops(); + if (isXRP((*before)[sfAmount])) + drops_ -= (*before)[sfAmount].xrp().drops(); break; default: break; @@ -181,7 +184,7 @@ XRPNotCreated::visitEntry( .drops(); break; case ltESCROW: - if (!isDelete) + if (!isDelete && isXRP((*after)[sfAmount])) drops_ += (*after)[sfAmount].xrp().drops(); break; default: @@ -321,15 +324,37 @@ NoZeroEscrow::visitEntry( std::shared_ptr const& after) { auto isBad = [](STAmount const& amount) { - if (!amount.native()) - return true; + // XRP case + if (amount.native()) + { + if (amount.xrp() <= XRPAmount{0}) + return true; - if (amount.xrp() <= XRPAmount{0}) - return true; + if (amount.xrp() >= INITIAL_XRP) + return true; + } + else + { + // IOU case + if (amount.holds()) + { + if (amount <= beast::zero) + return true; - if (amount.xrp() >= INITIAL_XRP) - return true; + if (badCurrency() == amount.getCurrency()) + return true; + } + // MPT case + if (amount.holds()) + { + if (amount <= beast::zero) + return true; + + if (amount.mpt() > MPTAmount{maxMPTokenAmount}) + return true; // LCOV_EXCL_LINE + } + } return false; }; @@ -338,14 +363,40 @@ NoZeroEscrow::visitEntry( if (after && after->getType() == ltESCROW) bad_ |= isBad((*after)[sfAmount]); + + auto checkAmount = [this](std::int64_t amount) { + if (amount > maxMPTokenAmount || amount < 0) + bad_ = true; + }; + + if (after && after->getType() == ltMPTOKEN_ISSUANCE) + { + auto const outstanding = (*after)[sfOutstandingAmount]; + checkAmount(outstanding); + if (auto const locked = (*after)[~sfLockedAmount]) + { + checkAmount(*locked); + bad_ = outstanding < *locked; + } + } + + if (after && after->getType() == ltMPTOKEN) + { + auto const mptAmount = (*after)[sfMPTAmount]; + checkAmount(mptAmount); + if (auto const locked = (*after)[~sfLockedAmount]) + { + checkAmount(*locked); + } + } } bool NoZeroEscrow::finalize( - STTx const&, + STTx const& txn, TER const, XRPAmount const, - ReadView const&, + ReadView const& rv, beast::Journal const& j) { if (bad_) @@ -1483,6 +1534,9 @@ ValidMPTIssuance::finalize( return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0 && mptokensCreated_ == 0 && mptokensDeleted_ == 0; } + + if (tx.getTxnType() == ttESCROW_FINISH) + return true; } if (mptIssuancesCreated_ != 0) @@ -1701,4 +1755,394 @@ ValidPseudoAccounts::finalize( return true; } +//------------------------------------------------------------------------------ + +void +ValidPermissionedDEX::visitEntry( + bool, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (after && after->getType() == ltDIR_NODE) + { + if (after->isFieldPresent(sfDomainID)) + domains_.insert(after->getFieldH256(sfDomainID)); + } + + if (after && after->getType() == ltOFFER) + { + if (after->isFieldPresent(sfDomainID)) + domains_.insert(after->getFieldH256(sfDomainID)); + else + regularOffers_ = true; + + // if a hybrid offer is missing domain or additional book, there's + // something wrong + if (after->isFlag(lsfHybrid) && + (!after->isFieldPresent(sfDomainID) || + !after->isFieldPresent(sfAdditionalBooks) || + after->getFieldArray(sfAdditionalBooks).size() > 1)) + badHybrids_ = true; + } +} + +bool +ValidPermissionedDEX::finalize( + STTx const& tx, + TER const result, + XRPAmount const, + ReadView const& view, + beast::Journal const& j) +{ + auto const txType = tx.getTxnType(); + if ((txType != ttPAYMENT && txType != ttOFFER_CREATE) || + result != tesSUCCESS) + return true; + + // For each offercreate transaction, check if + // permissioned offers are valid + if (txType == ttOFFER_CREATE && badHybrids_) + { + JLOG(j.fatal()) << "Invariant failed: hybrid offer is malformed"; + return false; + } + + if (!tx.isFieldPresent(sfDomainID)) + return true; + + auto const domain = tx.getFieldH256(sfDomainID); + + if (!view.exists(keylet::permissionedDomain(domain))) + { + JLOG(j.fatal()) << "Invariant failed: domain doesn't exist"; + return false; + } + + // for both payment and offercreate, there shouldn't be another domain + // that's different from the domain specified + for (auto const& d : domains_) + { + if (d != domain) + { + JLOG(j.fatal()) << "Invariant failed: transaction" + " consumed wrong domains"; + return false; + } + } + + if (regularOffers_) + { + JLOG(j.fatal()) << "Invariant failed: domain transaction" + " affected regular offers"; + return false; + } + + return true; +} + +void +ValidAMM::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (isDelete) + return; + + if (after) + { + auto const type = after->getType(); + // AMM object changed + if (type == ltAMM) + { + ammAccount_ = after->getAccountID(sfAccount); + lptAMMBalanceAfter_ = after->getFieldAmount(sfLPTokenBalance); + } + // AMM pool changed + else if ( + (type == ltRIPPLE_STATE && after->getFlags() & lsfAMMNode) || + (type == ltACCOUNT_ROOT && after->isFieldPresent(sfAMMID))) + { + ammPoolChanged_ = true; + } + } + + if (before) + { + // AMM object changed + if (before->getType() == ltAMM) + { + lptAMMBalanceBefore_ = before->getFieldAmount(sfLPTokenBalance); + } + } +} + +static bool +validBalances( + STAmount const& amount, + STAmount const& amount2, + STAmount const& lptAMMBalance, + ValidAMM::ZeroAllowed zeroAllowed) +{ + bool const positive = amount > beast::zero && amount2 > beast::zero && + lptAMMBalance > beast::zero; + if (zeroAllowed == ValidAMM::ZeroAllowed::Yes) + return positive || + (amount == beast::zero && amount2 == beast::zero && + lptAMMBalance == beast::zero); + return positive; +} + +bool +ValidAMM::finalizeVote(bool enforce, beast::Journal const& j) const +{ + if (lptAMMBalanceAfter_ != lptAMMBalanceBefore_ || ammPoolChanged_) + { + // LPTokens and the pool can not change on vote + // LCOV_EXCL_START + JLOG(j.error()) << "AMMVote invariant failed: " + << lptAMMBalanceBefore_.value_or(STAmount{}) << " " + << lptAMMBalanceAfter_.value_or(STAmount{}) << " " + << ammPoolChanged_; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + + return true; +} + +bool +ValidAMM::finalizeBid(bool enforce, beast::Journal const& j) const +{ + if (ammPoolChanged_) + { + // The pool can not change on bid + // LCOV_EXCL_START + JLOG(j.error()) << "AMMBid invariant failed: pool changed"; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + // LPTokens are burnt, therefore there should be fewer LPTokens + else if ( + lptAMMBalanceBefore_ && lptAMMBalanceAfter_ && + (*lptAMMBalanceAfter_ > *lptAMMBalanceBefore_ || + *lptAMMBalanceAfter_ <= beast::zero)) + { + // LCOV_EXCL_START + JLOG(j.error()) << "AMMBid invariant failed: " << *lptAMMBalanceBefore_ + << " " << *lptAMMBalanceAfter_; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + + return true; +} + +bool +ValidAMM::finalizeCreate( + STTx const& tx, + ReadView const& view, + bool enforce, + beast::Journal const& j) const +{ + if (!ammAccount_) + { + // LCOV_EXCL_START + JLOG(j.error()) + << "AMMCreate invariant failed: AMM object is not created"; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + else + { + auto const [amount, amount2] = ammPoolHolds( + view, + *ammAccount_, + tx[sfAmount].get(), + tx[sfAmount2].get(), + fhIGNORE_FREEZE, + j); + // Create invariant: + // sqrt(amount * amount2) == LPTokens + // all balances are greater than zero + if (!validBalances( + amount, amount2, *lptAMMBalanceAfter_, ZeroAllowed::No) || + ammLPTokens(amount, amount2, lptAMMBalanceAfter_->issue()) != + *lptAMMBalanceAfter_) + { + JLOG(j.error()) << "AMMCreate invariant failed: " << amount << " " + << amount2 << " " << *lptAMMBalanceAfter_; + if (enforce) + return false; + } + } + + return true; +} + +bool +ValidAMM::finalizeDelete(bool enforce, TER res, beast::Journal const& j) const +{ + if (ammAccount_) + { + // LCOV_EXCL_START + std::string const msg = (res == tesSUCCESS) + ? "AMM object is not deleted on tesSUCCESS" + : "AMM object is changed on tecINCOMPLETE"; + JLOG(j.error()) << "AMMDelete invariant failed: " << msg; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + + return true; +} + +bool +ValidAMM::finalizeDEX(bool enforce, beast::Journal const& j) const +{ + if (ammAccount_) + { + // LCOV_EXCL_START + JLOG(j.error()) << "AMM swap invariant failed: AMM object changed"; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + + return true; +} + +bool +ValidAMM::generalInvariant( + ripple::STTx const& tx, + ripple::ReadView const& view, + ZeroAllowed zeroAllowed, + beast::Journal const& j) const +{ + auto const [amount, amount2] = ammPoolHolds( + view, + *ammAccount_, + tx[sfAsset].get(), + tx[sfAsset2].get(), + fhIGNORE_FREEZE, + j); + // Deposit and Withdrawal invariant: + // sqrt(amount * amount2) >= LPTokens + // all balances are greater than zero + // unless on last withdrawal + 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::zero && + withinRelativeDistance( + poolProductMean, Number{*lptAMMBalanceAfter_}, Number{1, -11}); + }; + if (!nonNegativeBalances || + (!strongInvariantCheck && !weakInvariantCheck())) + { + JLOG(j.error()) << "AMM " << tx.getTxnType() << " invariant failed: " + << tx.getHash(HashPrefix::transactionID) << " " + << ammPoolChanged_ << " " << amount << " " << amount2 + << " " << poolProductMean << " " + << lptAMMBalanceAfter_->getText() << " " + << ((*lptAMMBalanceAfter_ == beast::zero) + ? Number{1} + : ((*lptAMMBalanceAfter_ - poolProductMean) / + poolProductMean)); + return false; + } + + return true; +} + +bool +ValidAMM::finalizeDeposit( + ripple::STTx const& tx, + ripple::ReadView const& view, + bool enforce, + beast::Journal const& j) const +{ + if (!ammAccount_) + { + // LCOV_EXCL_START + JLOG(j.error()) << "AMMDeposit invariant failed: AMM object is deleted"; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + else if (!generalInvariant(tx, view, ZeroAllowed::No, j) && enforce) + return false; + + return true; +} + +bool +ValidAMM::finalizeWithdraw( + ripple::STTx const& tx, + ripple::ReadView const& view, + bool enforce, + beast::Journal const& j) const +{ + if (!ammAccount_) + { + // Last Withdraw or Clawback deleted AMM + } + else if (!generalInvariant(tx, view, ZeroAllowed::Yes, j)) + { + if (enforce) + return false; + } + + return true; +} + +bool +ValidAMM::finalize( + STTx const& tx, + TER const result, + XRPAmount const, + ReadView const& view, + beast::Journal const& j) +{ + // Delete may return tecINCOMPLETE if there are too many + // trustlines to delete. + if (result != tesSUCCESS && result != tecINCOMPLETE) + return true; + + bool const enforce = view.rules().enabled(fixAMMv1_3); + + switch (tx.getTxnType()) + { + case ttAMM_CREATE: + return finalizeCreate(tx, view, enforce, j); + case ttAMM_DEPOSIT: + return finalizeDeposit(tx, view, enforce, j); + case ttAMM_CLAWBACK: + case ttAMM_WITHDRAW: + return finalizeWithdraw(tx, view, enforce, j); + case ttAMM_BID: + return finalizeBid(enforce, j); + case ttAMM_VOTE: + return finalizeVote(enforce, j); + case ttAMM_DELETE: + return finalizeDelete(enforce, result, j); + case ttCHECK_CASH: + case ttOFFER_CREATE: + case ttPAYMENT: + return finalizeDEX(enforce, j); + default: + break; + } + + return true; +} + } // namespace ripple diff --git a/src/xrpld/app/tx/detail/InvariantCheck.h b/src/xrpld/app/tx/detail/InvariantCheck.h index 58dbc23066..5444f2f3a9 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.h +++ b/src/xrpld/app/tx/detail/InvariantCheck.h @@ -28,6 +28,7 @@ #include #include +#include namespace ripple { @@ -646,6 +647,91 @@ public: beast::Journal const&); }; +class ValidPermissionedDEX +{ + bool regularOffers_ = false; + bool badHybrids_ = false; + hash_set domains_; + +public: + void + visitEntry( + bool, + std::shared_ptr const&, + std::shared_ptr const&); + + bool + finalize( + STTx const&, + TER const, + XRPAmount const, + ReadView const&, + beast::Journal const&); +}; + +class ValidAMM +{ + std::optional ammAccount_; + std::optional lptAMMBalanceAfter_; + std::optional lptAMMBalanceBefore_; + bool ammPoolChanged_; + +public: + enum class ZeroAllowed : bool { No = false, Yes = true }; + + ValidAMM() : ammPoolChanged_{false} + { + } + void + visitEntry( + bool, + std::shared_ptr const&, + std::shared_ptr const&); + + bool + finalize( + STTx const&, + TER const, + XRPAmount const, + ReadView const&, + beast::Journal const&); + +private: + bool + finalizeBid(bool enforce, beast::Journal const&) const; + bool + finalizeVote(bool enforce, beast::Journal const&) const; + bool + finalizeCreate( + STTx const&, + ReadView const&, + bool enforce, + beast::Journal const&) const; + bool + finalizeDelete(bool enforce, TER res, beast::Journal const&) const; + bool + finalizeDeposit( + STTx const&, + ReadView const&, + bool enforce, + beast::Journal const&) const; + // Includes clawback + bool + finalizeWithdraw( + STTx const&, + ReadView const&, + bool enforce, + beast::Journal const&) const; + bool + finalizeDEX(bool enforce, beast::Journal const&) const; + bool + generalInvariant( + STTx const&, + ReadView const&, + ZeroAllowed zeroAllowed, + beast::Journal const&) const; +}; + // additional invariant checks can be declared above and then added to this // tuple using InvariantChecks = std::tuple< @@ -666,6 +752,8 @@ using InvariantChecks = std::tuple< ValidClawback, ValidMPTIssuance, ValidPermissionedDomain, + ValidPermissionedDEX, + ValidAMM, ValidPseudoAccounts>; /** diff --git a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp index 748c05869f..77b21b65f3 100644 --- a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp +++ b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp @@ -83,6 +83,15 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx) return tecHAS_OBLIGATIONS; } + if ((*sleMpt)[~sfLockedAmount].value_or(0) != 0) + { + auto const sleMptIssuance = ctx.view.read( + keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID])); + if (!sleMptIssuance) + return tefINTERNAL; // LCOV_EXCL_LINE + + return tecHAS_OBLIGATIONS; + } if (ctx.view.rules().enabled(featureSingleAssetVault) && sleMpt->isFlag(lsfMPTLocked)) return tecNO_PERMISSION; @@ -140,6 +149,32 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx) return tesSUCCESS; } +TER +MPTokenAuthorize::createMPToken( + ApplyView& view, + MPTID const& mptIssuanceID, + AccountID const& account, + std::uint32_t const flags) +{ + auto const mptokenKey = keylet::mptoken(mptIssuanceID, account); + + auto const ownerNode = view.dirInsert( + keylet::ownerDir(account), mptokenKey, describeOwnerDir(account)); + + if (!ownerNode) + return tecDIR_FULL; // LCOV_EXCL_LINE + + auto mptoken = std::make_shared(mptokenKey); + (*mptoken)[sfAccount] = account; + (*mptoken)[sfMPTokenIssuanceID] = mptIssuanceID; + (*mptoken)[sfFlags] = flags; + (*mptoken)[sfOwnerNode] = *ownerNode; + + view.insert(mptoken); + + return tesSUCCESS; +} + TER MPTokenAuthorize::authorize( ApplyView& view, diff --git a/src/xrpld/app/tx/detail/MPTokenAuthorize.h b/src/xrpld/app/tx/detail/MPTokenAuthorize.h index e2b135a22a..a81dc7dea2 100644 --- a/src/xrpld/app/tx/detail/MPTokenAuthorize.h +++ b/src/xrpld/app/tx/detail/MPTokenAuthorize.h @@ -54,6 +54,13 @@ public: beast::Journal journal, MPTAuthorizeArgs const& args); + static TER + createMPToken( + ApplyView& view, + MPTID const& mptIssuanceID, + AccountID const& account, + std::uint32_t const flags); + TER doApply() override; }; diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp index d06ea3473e..e2b87dbd79 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp @@ -58,6 +58,9 @@ MPTokenIssuanceDestroy::preclaim(PreclaimContext const& ctx) if ((*sleMPT)[sfOutstandingAmount] != 0) return tecHAS_OBLIGATIONS; + if ((*sleMPT)[~sfLockedAmount].value_or(0) != 0) + return tecHAS_OBLIGATIONS; // LCOV_EXCL_LINE + return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp index 85a1f6cf1a..06ea089526 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp @@ -62,7 +62,7 @@ MPTokenIssuanceSet::checkPermission(ReadView const& view, STTx const& tx) auto const sle = view.read(delegateKey); if (!sle) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; if (checkTxPermission(sle, tx) == tesSUCCESS) return tesSUCCESS; @@ -72,18 +72,18 @@ MPTokenIssuanceSet::checkPermission(ReadView const& view, STTx const& tx) // this is added in case more flags will be added for MPTokenIssuanceSet // in the future. Currently unreachable. if (txFlags & tfMPTokenIssuanceSetPermissionMask) - return tecNO_PERMISSION; // LCOV_EXCL_LINE + return tecNO_DELEGATE_PERMISSION; // LCOV_EXCL_LINE std::unordered_set granularPermissions; loadGranularPermission(sle, ttMPTOKEN_ISSUANCE_SET, granularPermissions); if (txFlags & tfMPTLock && !granularPermissions.contains(MPTokenIssuanceLock)) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; if (txFlags & tfMPTUnlock && !granularPermissions.contains(MPTokenIssuanceUnlock)) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp b/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp index 4c5fdb7683..ab74e5ac39 100644 --- a/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp +++ b/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp @@ -160,6 +160,27 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx) if ((*so)[sfAmount] > (*bo)[sfAmount] - *brokerFee) return tecINSUFFICIENT_PAYMENT; + + // Check if broker is allowed to receive the fee with these IOUs. + if (!brokerFee->native() && + ctx.view.rules().enabled(fixEnforceNFTokenTrustlineV2)) + { + auto res = nft::checkTrustlineAuthorized( + ctx.view, + ctx.tx[sfAccount], + ctx.j, + brokerFee->asset().get()); + if (res != tesSUCCESS) + return res; + + res = nft::checkTrustlineDeepFrozen( + ctx.view, + ctx.tx[sfAccount], + ctx.j, + brokerFee->asset().get()); + if (res != tesSUCCESS) + return res; + } } } @@ -208,6 +229,38 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx) fhZERO_IF_FROZEN, ctx.j) < needed) return tecINSUFFICIENT_FUNDS; + + // Check that the account accepting the buy offer (he's selling the NFT) + // is allowed to receive IOUs. Also check that this offer's creator is + // authorized. But we need to exclude the case when the transaction is + // created by the broker. + if (ctx.view.rules().enabled(fixEnforceNFTokenTrustlineV2) && + !needed.native()) + { + auto res = nft::checkTrustlineAuthorized( + ctx.view, bo->at(sfOwner), ctx.j, needed.asset().get()); + if (res != tesSUCCESS) + return res; + + if (!so) + { + res = nft::checkTrustlineAuthorized( + ctx.view, + ctx.tx[sfAccount], + ctx.j, + needed.asset().get()); + if (res != tesSUCCESS) + return res; + + res = nft::checkTrustlineDeepFrozen( + ctx.view, + ctx.tx[sfAccount], + ctx.j, + needed.asset().get()); + if (res != tesSUCCESS) + return res; + } + } } if (so) @@ -270,42 +323,74 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx) } // Make sure that we are allowed to hold what the taker will pay us. - // This is a similar approach taken by usual offers. if (!needed.native()) { - auto const result = checkAcceptAsset( - ctx.view, - ctx.flags, - (*so)[sfOwner], - ctx.j, - needed.asset().get()); - if (result != tesSUCCESS) - return result; + if (ctx.view.rules().enabled(fixEnforceNFTokenTrustlineV2)) + { + auto res = nft::checkTrustlineAuthorized( + ctx.view, + (*so)[sfOwner], + ctx.j, + needed.asset().get()); + if (res != tesSUCCESS) + return res; + + if (!bo) + { + res = nft::checkTrustlineAuthorized( + ctx.view, + ctx.tx[sfAccount], + ctx.j, + needed.asset().get()); + if (res != tesSUCCESS) + return res; + } + } + + auto const res = nft::checkTrustlineDeepFrozen( + ctx.view, (*so)[sfOwner], ctx.j, needed.asset().get()); + if (res != tesSUCCESS) + return res; } } - // Fix a bug where the transfer of an NFToken with a transfer fee could - // give the NFToken issuer an undesired trust line. - if (ctx.view.rules().enabled(fixEnforceNFTokenTrustline)) + // Additional checks are required in case a minter set a transfer fee for + // this nftoken + auto const& offer = bo ? bo : so; + if (!offer) + // Purely defensive, should be caught in preflight. + return tecINTERNAL; + + auto const& tokenID = offer->at(sfNFTokenID); + auto const& amount = offer->at(sfAmount); + auto const nftMinter = nft::getIssuer(tokenID); + + if (nft::getTransferFee(tokenID) != 0 && !amount.native()) { - std::shared_ptr const& offer = bo ? bo : so; - if (!offer) - // Should be caught in preflight. - return tecINTERNAL; - - uint256 const& tokenID = offer->at(sfNFTokenID); - STAmount const& amount = offer->at(sfAmount); - if (nft::getTransferFee(tokenID) != 0 && + // Fix a bug where the transfer of an NFToken with a transfer fee could + // give the NFToken issuer an undesired trust line. + // Issuer doesn't need a trust line to accept their own currency. + if (ctx.view.rules().enabled(fixEnforceNFTokenTrustline) && (nft::getFlags(tokenID) & nft::flagCreateTrustLines) == 0 && - !amount.native()) + nftMinter != amount.getIssuer() && + !ctx.view.read(keylet::line(nftMinter, amount.issue()))) + return tecNO_LINE; + + // Check that the issuer is allowed to receive IOUs. + if (ctx.view.rules().enabled(fixEnforceNFTokenTrustlineV2)) { - auto const issuer = nft::getIssuer(tokenID); - // Issuer doesn't need a trust line to accept their own currency. - if (issuer != amount.getIssuer() && - !ctx.view.read(keylet::line(issuer, amount.issue()))) - return tecNO_LINE; + auto res = nft::checkTrustlineAuthorized( + ctx.view, nftMinter, ctx.j, amount.asset().get()); + if (res != tesSUCCESS) + return res; + + res = nft::checkTrustlineDeepFrozen( + ctx.view, nftMinter, ctx.j, amount.asset().get()); + if (res != tesSUCCESS) + return res; } } + return tesSUCCESS; } @@ -524,62 +609,4 @@ NFTokenAcceptOffer::doApply() return tecINTERNAL; } -TER -NFTokenAcceptOffer::checkAcceptAsset( - ReadView const& view, - ApplyFlags const flags, - AccountID const id, - beast::Journal const j, - Issue const& issue) -{ - // Only valid for custom currencies - - if (!view.rules().enabled(featureDeepFreeze)) - { - return tesSUCCESS; - } - - XRPL_ASSERT( - !isXRP(issue.currency), - "NFTokenAcceptOffer::checkAcceptAsset : valid to check."); - auto const issuerAccount = view.read(keylet::account(issue.account)); - - if (!issuerAccount) - { - JLOG(j.debug()) - << "delay: can't receive IOUs from non-existent issuer: " - << to_string(issue.account); - - return tecNO_ISSUER; - } - - // An account can not create a trustline to itself, so no line can exist - // to be frozen. Additionally, an issuer can always accept its own - // issuance. - if (issue.account == id) - { - return tesSUCCESS; - } - - auto const trustLine = - view.read(keylet::line(id, issue.account, issue.currency)); - - if (!trustLine) - { - return tesSUCCESS; - } - - // There's no difference which side enacted deep freeze, accepting - // tokens shouldn't be possible. - bool const deepFrozen = - (*trustLine)[sfFlags] & (lsfLowDeepFreeze | lsfHighDeepFreeze); - - if (deepFrozen) - { - return tecFROZEN; - } - - return tesSUCCESS; -} - } // namespace ripple diff --git a/src/xrpld/app/tx/detail/NFTokenAcceptOffer.h b/src/xrpld/app/tx/detail/NFTokenAcceptOffer.h index 6a594e2b2c..dff3febbb2 100644 --- a/src/xrpld/app/tx/detail/NFTokenAcceptOffer.h +++ b/src/xrpld/app/tx/detail/NFTokenAcceptOffer.h @@ -44,14 +44,6 @@ private: AccountID const& seller, uint256 const& nfTokenID); - static TER - checkAcceptAsset( - ReadView const& view, - ApplyFlags const flags, - AccountID const id, - beast::Journal const j, - Issue const& issue); - public: static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; diff --git a/src/xrpld/app/tx/detail/NFTokenUtils.cpp b/src/xrpld/app/tx/detail/NFTokenUtils.cpp index 9c9754aa95..4866a3b385 100644 --- a/src/xrpld/app/tx/detail/NFTokenUtils.cpp +++ b/src/xrpld/app/tx/detail/NFTokenUtils.cpp @@ -1004,6 +1004,18 @@ tokenOfferCreatePreclaim( } } + if (view.rules().enabled(fixEnforceNFTokenTrustlineV2) && !amount.native()) + { + // If this is a sell offer, check that the account is allowed to + // receive IOUs. If this is a buy offer, we have to check that trustline + // is authorized, even though we previosly checked it's balance via + // accountHolds. This is due to a possibility of existence of + // unauthorized trustlines with balance + auto const res = nft::checkTrustlineAuthorized( + view, acctID, j, amount.asset().get()); + if (res != tesSUCCESS) + return res; + } return tesSUCCESS; } @@ -1081,5 +1093,115 @@ tokenOfferCreateApply( return tesSUCCESS; } +TER +checkTrustlineAuthorized( + ReadView const& view, + AccountID const id, + beast::Journal const j, + Issue const& issue) +{ + // Only valid for custom currencies + XRPL_ASSERT( + !isXRP(issue.currency), + "ripple::nft::checkTrustlineAuthorized : valid to check."); + + if (view.rules().enabled(fixEnforceNFTokenTrustlineV2)) + { + auto const issuerAccount = view.read(keylet::account(issue.account)); + if (!issuerAccount) + { + JLOG(j.debug()) << "ripple::nft::checkTrustlineAuthorized: can't " + "receive IOUs from non-existent issuer: " + << to_string(issue.account); + + return tecNO_ISSUER; + } + + // An account can not create a trustline to itself, so no line can + // exist to be authorized. Additionally, an issuer can always accept + // its own issuance. + if (issue.account == id) + { + return tesSUCCESS; + } + + if (issuerAccount->isFlag(lsfRequireAuth)) + { + auto const trustLine = + view.read(keylet::line(id, issue.account, issue.currency)); + + if (!trustLine) + { + return tecNO_LINE; + } + + // Entries have a canonical representation, determined by a + // lexicographical "greater than" comparison employing strict + // weak ordering. Determine which entry we need to access. + if (!trustLine->isFlag( + id > issue.account ? lsfLowAuth : lsfHighAuth)) + { + return tecNO_AUTH; + } + } + } + + return tesSUCCESS; +} + +TER +checkTrustlineDeepFrozen( + ReadView const& view, + AccountID const id, + beast::Journal const j, + Issue const& issue) +{ + // Only valid for custom currencies + XRPL_ASSERT( + !isXRP(issue.currency), + "ripple::nft::checkTrustlineDeepFrozen : valid to check."); + + if (view.rules().enabled(featureDeepFreeze)) + { + auto const issuerAccount = view.read(keylet::account(issue.account)); + if (!issuerAccount) + { + JLOG(j.debug()) << "ripple::nft::checkTrustlineDeepFrozen: can't " + "receive IOUs from non-existent issuer: " + << to_string(issue.account); + + return tecNO_ISSUER; + } + + // An account can not create a trustline to itself, so no line can + // exist to be frozen. Additionally, an issuer can always accept its + // own issuance. + if (issue.account == id) + { + return tesSUCCESS; + } + + auto const trustLine = + view.read(keylet::line(id, issue.account, issue.currency)); + + if (!trustLine) + { + return tesSUCCESS; + } + + // There's no difference which side enacted deep freeze, accepting + // tokens shouldn't be possible. + bool const deepFrozen = + (*trustLine)[sfFlags] & (lsfLowDeepFreeze | lsfHighDeepFreeze); + + if (deepFrozen) + { + return tecFROZEN; + } + } + + return tesSUCCESS; +} + } // namespace nft } // namespace ripple diff --git a/src/xrpld/app/tx/detail/NFTokenUtils.h b/src/xrpld/app/tx/detail/NFTokenUtils.h index 38ced59e9c..7ee0541984 100644 --- a/src/xrpld/app/tx/detail/NFTokenUtils.h +++ b/src/xrpld/app/tx/detail/NFTokenUtils.h @@ -152,6 +152,20 @@ tokenOfferCreateApply( beast::Journal j, std::uint32_t txFlags = lsfSellNFToken); +TER +checkTrustlineAuthorized( + ReadView const& view, + AccountID const id, + beast::Journal const j, + Issue const& issue); + +TER +checkTrustlineDeepFrozen( + ReadView const& view, + AccountID const id, + beast::Journal const j, + Issue const& issue); + } // namespace nft } // namespace ripple diff --git a/src/xrpld/app/tx/detail/Offer.h b/src/xrpld/app/tx/detail/Offer.h index abc0212335..d6ff4c7699 100644 --- a/src/xrpld/app/tx/detail/Offer.h +++ b/src/xrpld/app/tx/detail/Offer.h @@ -22,6 +22,7 @@ #include +#include #include #include #include @@ -170,8 +171,24 @@ public: * always returns true. */ bool - checkInvariant(TAmounts const&, beast::Journal j) const + checkInvariant(TAmounts const& consumed, beast::Journal j) const { + if (!isFeatureEnabled(fixAMMv1_3)) + return true; + + if (consumed.in > m_amounts.in || consumed.out > m_amounts.out) + { + // LCOV_EXCL_START + JLOG(j.error()) + << "AMMOffer::checkInvariant failed: consumed " + << to_string(consumed.in) << " " << to_string(consumed.out) + << " amounts " << to_string(m_amounts.in) << " " + << to_string(m_amounts.out); + + return false; + // LCOV_EXCL_STOP + } + return true; } }; diff --git a/src/xrpld/app/tx/detail/OfferStream.cpp b/src/xrpld/app/tx/detail/OfferStream.cpp index 7640cca206..55993f5c5f 100644 --- a/src/xrpld/app/tx/detail/OfferStream.cpp +++ b/src/xrpld/app/tx/detail/OfferStream.cpp @@ -17,10 +17,13 @@ */ //============================================================================== +#include #include +#include #include #include +#include namespace ripple { @@ -288,6 +291,17 @@ TOfferStreamBase::step() continue; } + if (entry->isFieldPresent(sfDomainID) && + !permissioned_dex::offerInDomain( + view_, entry->key(), entry->getFieldH256(sfDomainID), j_)) + { + JLOG(j_.trace()) + << "Removing offer no longer in domain " << entry->key(); + permRmOffer(entry->key()); + offer_ = TOffer{}; + continue; + } + // Calculate owner funds ownerFunds_ = accountFundsHelper( view_, diff --git a/src/xrpld/app/tx/detail/PayChan.cpp b/src/xrpld/app/tx/detail/PayChan.cpp index a42902f6ac..d9e53ac75c 100644 --- a/src/xrpld/app/tx/detail/PayChan.cpp +++ b/src/xrpld/app/tx/detail/PayChan.cpp @@ -473,7 +473,8 @@ PayChanClaim::preflight(PreflightContext const& ctx) return temBAD_SIGNATURE; } - if (auto const err = credentials::checkFields(ctx); !isTesSuccess(err)) + if (auto const err = credentials::checkFields(ctx.tx, ctx.j); + !isTesSuccess(err)) return err; return preflight2(ctx); @@ -485,7 +486,8 @@ PayChanClaim::preclaim(PreclaimContext const& ctx) if (!ctx.view.rules().enabled(featureCredentials)) return Transactor::preclaim(ctx); - if (auto const err = credentials::valid(ctx, ctx.tx[sfAccount]); + if (auto const err = + credentials::valid(ctx.tx, ctx.view, ctx.tx[sfAccount], ctx.j); !isTesSuccess(err)) return err; @@ -554,7 +556,8 @@ PayChanClaim::doApply() if (depositAuth) { - if (auto err = verifyDepositPreauth(ctx_, txAccount, dst, sled); + if (auto err = verifyDepositPreauth( + ctx_.tx, ctx_.view(), txAccount, dst, sled, ctx_.journal); !isTesSuccess(err)) return err; } diff --git a/src/xrpld/app/tx/detail/Payment.cpp b/src/xrpld/app/tx/detail/Payment.cpp index a97e472841..692e03109e 100644 --- a/src/xrpld/app/tx/detail/Payment.cpp +++ b/src/xrpld/app/tx/detail/Payment.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -71,6 +72,10 @@ Payment::preflight(PreflightContext const& ctx) !ctx.rules.enabled(featureCredentials)) return temDISABLED; + if (ctx.tx.isFieldPresent(sfDomainID) && + !ctx.rules.enabled(featurePermissionedDEX)) + return temDISABLED; + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; @@ -233,7 +238,8 @@ Payment::preflight(PreflightContext const& ctx) } } - if (auto const err = credentials::checkFields(ctx); !isTesSuccess(err)) + if (auto const err = credentials::checkFields(ctx.tx, ctx.j); + !isTesSuccess(err)) return err; return preflight2(ctx); @@ -250,7 +256,7 @@ Payment::checkPermission(ReadView const& view, STTx const& tx) auto const sle = view.read(delegateKey); if (!sle) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; if (checkTxPermission(sle, tx) == tesSUCCESS) return tesSUCCESS; @@ -269,7 +275,7 @@ Payment::checkPermission(ReadView const& view, STTx const& tx) amountIssue.account == tx[sfDestination]) return tesSUCCESS; - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; } TER @@ -353,10 +359,22 @@ Payment::preclaim(PreclaimContext const& ctx) } } - if (auto const err = credentials::valid(ctx, ctx.tx[sfAccount]); + if (auto const err = + credentials::valid(ctx.tx, ctx.view, ctx.tx[sfAccount], ctx.j); !isTesSuccess(err)) return err; + if (ctx.tx.isFieldPresent(sfDomainID)) + { + if (!permissioned_dex::accountInDomain( + ctx.view, ctx.tx[sfAccount], ctx.tx[sfDomainID])) + return tecNO_PERMISSION; + + if (!permissioned_dex::accountInDomain( + ctx.view, ctx.tx[sfDestination], ctx.tx[sfDomainID])) + return tecNO_PERMISSION; + } + return tesSUCCESS; } @@ -434,8 +452,13 @@ Payment::doApply() // 1. If Account == Destination, or // 2. If Account is deposit preauthorized by destination. - if (auto err = - verifyDepositPreauth(ctx_, account_, dstAccountID, sleDst); + if (auto err = verifyDepositPreauth( + ctx_.tx, + ctx_.view(), + account_, + dstAccountID, + sleDst, + ctx_.journal); !isTesSuccess(err)) return err; } @@ -458,6 +481,7 @@ Payment::doApply() dstAccountID, account_, ctx_.tx.getFieldPathSet(sfPaths), + ctx_.tx[~sfDomainID], ctx_.app.logs(), &rcInput); // VFALCO NOTE We might not need to apply, depending @@ -504,8 +528,13 @@ Payment::doApply() ter != tesSUCCESS) return ter; - if (auto err = - verifyDepositPreauth(ctx_, account_, dstAccountID, sleDst); + if (auto err = verifyDepositPreauth( + ctx_.tx, + ctx_.view(), + account_, + dstAccountID, + sleDst, + ctx_.journal); !isTesSuccess(err)) return err; @@ -627,8 +656,13 @@ Payment::doApply() if (dstAmount > dstReserve || sleDst->getFieldAmount(sfBalance) > dstReserve) { - if (auto err = - verifyDepositPreauth(ctx_, account_, dstAccountID, sleDst); + if (auto err = verifyDepositPreauth( + ctx_.tx, + ctx_.view(), + account_, + dstAccountID, + sleDst, + ctx_.journal); !isTesSuccess(err)) return err; } diff --git a/src/xrpld/app/tx/detail/SetAccount.cpp b/src/xrpld/app/tx/detail/SetAccount.cpp index 599819151a..ec618981c1 100644 --- a/src/xrpld/app/tx/detail/SetAccount.cpp +++ b/src/xrpld/app/tx/detail/SetAccount.cpp @@ -202,7 +202,7 @@ SetAccount::checkPermission(ReadView const& view, STTx const& tx) auto const sle = view.read(delegateKey); if (!sle) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; std::unordered_set granularPermissions; loadGranularPermission(sle, ttACCOUNT_SET, granularPermissions); @@ -214,32 +214,32 @@ SetAccount::checkPermission(ReadView const& view, STTx const& tx) // 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 || uTxFlags != tfFullyCanonicalSig) - return tecNO_PERMISSION; + if (uSetFlag != 0 || uClearFlag != 0 || uTxFlags & tfUniversalMask) + return tecNO_DELEGATE_PERMISSION; if (tx.isFieldPresent(sfEmailHash) && !granularPermissions.contains(AccountEmailHashSet)) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; if (tx.isFieldPresent(sfWalletLocator) || tx.isFieldPresent(sfNFTokenMinter)) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; if (tx.isFieldPresent(sfMessageKey) && !granularPermissions.contains(AccountMessageKeySet)) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; if (tx.isFieldPresent(sfDomain) && !granularPermissions.contains(AccountDomainSet)) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; if (tx.isFieldPresent(sfTransferRate) && !granularPermissions.contains(AccountTransferRateSet)) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; if (tx.isFieldPresent(sfTickSize) && !granularPermissions.contains(AccountTickSizeSet)) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; return tesSUCCESS; } @@ -650,6 +650,15 @@ SetAccount::doApply() uFlagsOut &= ~lsfDisallowIncomingTrustline; } + // Set or clear flags for disallowing escrow + if (ctx_.view().rules().enabled(featureTokenEscrow)) + { + if (uSetFlag == asfAllowTrustLineLocking) + uFlagsOut |= lsfAllowTrustLineLocking; + else if (uClearFlag == asfAllowTrustLineLocking) + uFlagsOut &= ~lsfAllowTrustLineLocking; + } + // Set flag for clawback if (ctx_.view().rules().enabled(featureClawback) && uSetFlag == asfAllowTrustLineClawback) diff --git a/src/xrpld/app/tx/detail/SetTrust.cpp b/src/xrpld/app/tx/detail/SetTrust.cpp index 5e83c201fa..d3b39aaf11 100644 --- a/src/xrpld/app/tx/detail/SetTrust.cpp +++ b/src/xrpld/app/tx/detail/SetTrust.cpp @@ -141,7 +141,7 @@ SetTrust::checkPermission(ReadView const& view, STTx const& tx) auto const sle = view.read(delegateKey); if (!sle) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; if (checkTxPermission(sle, tx) == tesSUCCESS) return tesSUCCESS; @@ -152,10 +152,10 @@ SetTrust::checkPermission(ReadView const& view, STTx const& tx) // TrustlineUnfreeze granular permission. Setting other flags returns // error. if (txFlags & tfTrustSetPermissionMask) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; if (tx.isFieldPresent(sfQualityIn) || tx.isFieldPresent(sfQualityOut)) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; auto const saLimitAmount = tx.getFieldAmount(sfLimitAmount); auto const sleRippleState = view.read(keylet::line( @@ -164,19 +164,19 @@ SetTrust::checkPermission(ReadView const& view, STTx const& tx) // if the trustline does not exist, granular permissions are // not allowed to create trustline if (!sleRippleState) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; std::unordered_set granularPermissions; loadGranularPermission(sle, ttTRUST_SET, granularPermissions); if (txFlags & tfSetfAuth && !granularPermissions.contains(TrustlineAuthorize)) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; if (txFlags & tfSetFreeze && !granularPermissions.contains(TrustlineFreeze)) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; if (txFlags & tfClearFreeze && !granularPermissions.contains(TrustlineUnfreeze)) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; // updating LimitAmount is not allowed only with granular permissions, // unless there's a new granular permission for this in the future. @@ -188,7 +188,7 @@ SetTrust::checkPermission(ReadView const& view, STTx const& tx) saLimitAllow.setIssuer(tx[sfAccount]); if (curLimit != saLimitAllow) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index 56d3302f46..c09fc4132b 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include namespace ripple { @@ -42,6 +43,13 @@ namespace ripple { NotTEC preflight0(PreflightContext const& ctx) { + if (isPseudoTx(ctx.tx) && ctx.tx.isFlag(tfInnerBatchTxn)) + { + JLOG(ctx.j.warn()) << "Pseudo transactions cannot contain the " + "tfInnerBatchTxn flag."; + return temINVALID_FLAG; + } + if (!isPseudoTx(ctx.tx) || ctx.tx.isFieldPresent(sfNetworkID)) { uint32_t nodeNID = ctx.app.config().NETWORK_ID; @@ -136,6 +144,14 @@ preflight1(PreflightContext const& ctx) ctx.tx.isFieldPresent(sfAccountTxnID)) return temINVALID; + if (ctx.tx.isFlag(tfInnerBatchTxn) && !ctx.rules.enabled(featureBatch)) + return temINVALID_FLAG; + + XRPL_ASSERT( + ctx.tx.isFlag(tfInnerBatchTxn) == ctx.parentBatchId.has_value() || + !ctx.rules.enabled(featureBatch), + "Inner batch transaction must have a parent batch ID."); + return tesSUCCESS; } @@ -168,6 +184,12 @@ preflight2(PreflightContext const& ctx) return temINVALID; // LCOV_EXCL_LINE } } + + if (!ctx.tx.getSigningPubKey().empty()) + { + // trying to single-sign _and_ multi-sign a transaction + return temINVALID; + } return tesSUCCESS; } @@ -176,25 +198,13 @@ preflight2(PreflightContext const& ctx) if (sigValid.first == Validity::SigBad) { JLOG(ctx.j.debug()) << "preflight2: bad signature. " << sigValid.second; - return temINVALID; + return temINVALID; // LCOV_EXCL_LINE } return tesSUCCESS; } //------------------------------------------------------------------------------ -PreflightContext::PreflightContext( - Application& app_, - STTx const& tx_, - Rules const& rules_, - ApplyFlags flags_, - beast::Journal j_) - : app(app_), tx(tx_), rules(rules_), flags(flags_), j(j_) -{ -} - -//------------------------------------------------------------------------------ - Transactor::Transactor(ApplyContext& ctx) : ctx_(ctx) , sink_(ctx.journal, to_short_string(ctx.tx.getTransactionID()) + " ") @@ -214,7 +224,7 @@ Transactor::checkPermission(ReadView const& view, STTx const& tx) auto const sle = view.read(delegateKey); if (!sle) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; return checkTxPermission(sle, tx); } @@ -254,6 +264,16 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee) return temBAD_FEE; auto const feePaid = ctx.tx[sfFee].xrp(); + + if (ctx.flags & tapBATCH) + { + if (feePaid == beast::zero) + return tesSUCCESS; + + JLOG(ctx.j.trace()) << "Batch: Fee must be zero."; + return temBAD_FEE; // LCOV_EXCL_LINE + } + if (!isLegalAmount(feePaid) || feePaid < beast::zero) return temBAD_FEE; @@ -286,9 +306,9 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee) if (balance < feePaid) { - JLOG(ctx.j.trace()) << "Insufficient balance:" - << " balance=" << to_string(balance) - << " paid=" << to_string(feePaid); + JLOG(ctx.j.trace()) + << "Insufficient balance:" << " balance=" << to_string(balance) + << " paid=" << to_string(feePaid); if ((balance > beast::zero) && !ctx.view.open()) { @@ -560,51 +580,118 @@ Transactor::apply() NotTEC Transactor::checkSign(PreclaimContext const& ctx) { - if (ctx.flags & tapDRY_RUN) - { - // This code must be different for `simulate` - // Since the public key may be empty even for single signing - if (ctx.tx.isFieldPresent(sfSigners)) - return checkMultiSign(ctx); - return checkSingleSign(ctx); - } - // If the pk is empty, then we must be multi-signing. - if (ctx.tx.getSigningPubKey().empty()) - return checkMultiSign(ctx); - - return checkSingleSign(ctx); -} - -NotTEC -Transactor::checkSingleSign(PreclaimContext const& ctx) -{ - // Check that the value in the signing key slot is a public key. auto const pkSigner = ctx.tx.getSigningPubKey(); - if (!(ctx.flags & tapDRY_RUN) && !publicKeyType(makeSlice(pkSigner))) + // Ignore signature check on batch inner transactions + if (ctx.tx.isFlag(tfInnerBatchTxn) && + ctx.view.rules().enabled(featureBatch)) + { + // Defensive Check: These values are also checked in Batch::preflight + if (ctx.tx.isFieldPresent(sfTxnSignature) || !pkSigner.empty() || + ctx.tx.isFieldPresent(sfSigners)) + { + return temINVALID_FLAG; // LCOV_EXCL_LINE + } + return tesSUCCESS; + } + + if ((ctx.flags & tapDRY_RUN) && pkSigner.empty() && + !ctx.tx.isFieldPresent(sfSigners)) + { + // simulate: skip signature validation when neither SigningPubKey nor + // Signers are provided + return tesSUCCESS; + } + + auto const idAccount = ctx.tx[~sfDelegate].value_or(ctx.tx[sfAccount]); + + // If the pk is empty and not simulate or simulate and signers, + // then we must be multi-signing. + if (ctx.tx.isFieldPresent(sfSigners)) + { + STArray const& txSigners(ctx.tx.getFieldArray(sfSigners)); + return checkMultiSign(ctx.view, idAccount, txSigners, ctx.flags, ctx.j); + } + + // Check Single Sign + XRPL_ASSERT( + !pkSigner.empty(), + "ripple::Transactor::checkSingleSign : non-empty signer or simulation"); + + if (!publicKeyType(makeSlice(pkSigner))) { JLOG(ctx.j.trace()) << "checkSingleSign: signing public key type is unknown"; return tefBAD_AUTH; // FIXME: should be better error! } - - // Look up the account. - auto const idAccount = ctx.tx.isFieldPresent(sfDelegate) - ? ctx.tx.getAccountID(sfDelegate) - : ctx.tx.getAccountID(sfAccount); + auto const idSigner = pkSigner.empty() + ? idAccount + : calcAccountID(PublicKey(makeSlice(pkSigner))); auto const sleAccount = ctx.view.read(keylet::account(idAccount)); if (!sleAccount) return terNO_ACCOUNT; - // This ternary is only needed to handle `simulate` - XRPL_ASSERT( - (ctx.flags & tapDRY_RUN) || !pkSigner.empty(), - "ripple::Transactor::checkSingleSign : non-empty signer or simulation"); - auto const idSigner = pkSigner.empty() - ? idAccount - : calcAccountID(PublicKey(makeSlice(pkSigner))); + return checkSingleSign( + idSigner, idAccount, sleAccount, ctx.view.rules(), ctx.j); +} + +NotTEC +Transactor::checkBatchSign(PreclaimContext const& ctx) +{ + NotTEC ret = tesSUCCESS; + STArray const& signers{ctx.tx.getFieldArray(sfBatchSigners)}; + for (auto const& signer : signers) + { + auto const idAccount = signer.getAccountID(sfAccount); + + Blob const& pkSigner = signer.getFieldVL(sfSigningPubKey); + if (pkSigner.empty()) + { + STArray const& txSigners(signer.getFieldArray(sfSigners)); + if (ret = checkMultiSign( + ctx.view, idAccount, txSigners, ctx.flags, ctx.j); + !isTesSuccess(ret)) + return ret; + } + else + { + // LCOV_EXCL_START + if (!publicKeyType(makeSlice(pkSigner))) + return tefBAD_AUTH; + // LCOV_EXCL_STOP + + auto const idSigner = calcAccountID(PublicKey(makeSlice(pkSigner))); + auto const sleAccount = ctx.view.read(keylet::account(idAccount)); + + // A batch can include transactions from an un-created account ONLY + // when the account master key is the signer + if (!sleAccount) + { + if (idAccount != idSigner) + return tefBAD_AUTH; + + return tesSUCCESS; + } + + if (ret = checkSingleSign( + idSigner, idAccount, sleAccount, ctx.view.rules(), ctx.j); + !isTesSuccess(ret)) + return ret; + } + } + return ret; +} + +NotTEC +Transactor::checkSingleSign( + AccountID const& idSigner, + AccountID const& idAccount, + std::shared_ptr sleAccount, + Rules const& rules, + beast::Journal j) +{ bool const isMasterDisabled = sleAccount->isFlag(lsfDisableMaster); - if (ctx.view.rules().enabled(fixMasterKeyAsRegularKey)) + if (rules.enabled(fixMasterKeyAsRegularKey)) { // Signed with regular key. if ((*sleAccount)[~sfRegularKey] == idSigner) @@ -641,16 +728,14 @@ Transactor::checkSingleSign(PreclaimContext const& ctx) else if (sleAccount->isFieldPresent(sfRegularKey)) { // Signing key does not match master or regular key. - JLOG(ctx.j.trace()) - << "checkSingleSign: Not authorized to use account."; + JLOG(j.trace()) << "checkSingleSign: Not authorized to use account."; return tefBAD_AUTH; } else { // No regular key on account and signing key does not match master key. // FIXME: Why differentiate this case from tefBAD_AUTH? - JLOG(ctx.j.trace()) - << "checkSingleSign: Not authorized to use account."; + JLOG(j.trace()) << "checkSingleSign: Not authorized to use account."; return tefBAD_AUTH_MASTER; } @@ -658,18 +743,20 @@ Transactor::checkSingleSign(PreclaimContext const& ctx) } NotTEC -Transactor::checkMultiSign(PreclaimContext const& ctx) +Transactor::checkMultiSign( + ReadView const& view, + AccountID const& id, + STArray const& txSigners, + ApplyFlags const& flags, + beast::Journal j) { - auto const id = ctx.tx.isFieldPresent(sfDelegate) - ? ctx.tx.getAccountID(sfDelegate) - : ctx.tx.getAccountID(sfAccount); // Get mTxnAccountID's SignerList and Quorum. std::shared_ptr sleAccountSigners = - ctx.view.read(keylet::signers(id)); + view.read(keylet::signers(id)); // If the signer list doesn't exist the account is not multi-signing. if (!sleAccountSigners) { - JLOG(ctx.j.trace()) + JLOG(j.trace()) << "applyTransaction: Invalid: Not a multi-signing account."; return tefNOT_MULTI_SIGNING; } @@ -684,12 +771,11 @@ Transactor::checkMultiSign(PreclaimContext const& ctx) "ripple::Transactor::checkMultiSign : signer list ID is 0"); auto accountSigners = - SignerEntries::deserialize(*sleAccountSigners, ctx.j, "ledger"); + SignerEntries::deserialize(*sleAccountSigners, j, "ledger"); if (!accountSigners) return accountSigners.error(); // Get the array of transaction signers. - STArray const& txSigners(ctx.tx.getFieldArray(sfSigners)); // Walk the accountSigners performing a variety of checks and see if // the quorum is met. @@ -708,7 +794,7 @@ Transactor::checkMultiSign(PreclaimContext const& ctx) { if (++iter == accountSigners->end()) { - JLOG(ctx.j.trace()) + JLOG(j.trace()) << "applyTransaction: Invalid SigningAccount.Account."; return tefBAD_SIGNATURE; } @@ -716,7 +802,7 @@ Transactor::checkMultiSign(PreclaimContext const& ctx) if (iter->account != txSignerAcctID) { // The SigningAccount is not in the SignerEntries. - JLOG(ctx.j.trace()) + JLOG(j.trace()) << "applyTransaction: Invalid SigningAccount.Account."; return tefBAD_SIGNATURE; } @@ -726,16 +812,17 @@ Transactor::checkMultiSign(PreclaimContext const& ctx) // public key. auto const spk = txSigner.getFieldVL(sfSigningPubKey); - if (!(ctx.flags & tapDRY_RUN) && !publicKeyType(makeSlice(spk))) + // spk being non-empty in non-simulate is checked in + // STTx::checkMultiSign + if (!spk.empty() && !publicKeyType(makeSlice(spk))) { - JLOG(ctx.j.trace()) + JLOG(j.trace()) << "checkMultiSign: signing public key type is unknown"; return tefBAD_SIGNATURE; } - // This ternary is only needed to handle `simulate` XRPL_ASSERT( - (ctx.flags & tapDRY_RUN) || !spk.empty(), + (flags & tapDRY_RUN) || !spk.empty(), "ripple::Transactor::checkMultiSign : non-empty signer or " "simulation"); AccountID const signingAcctIDFromPubKey = spk.empty() @@ -767,7 +854,7 @@ Transactor::checkMultiSign(PreclaimContext const& ctx) // In any of these cases we need to know whether the account is in // the ledger. Determine that now. - auto sleTxSignerRoot = ctx.view.read(keylet::account(txSignerAcctID)); + auto const sleTxSignerRoot = view.read(keylet::account(txSignerAcctID)); if (signingAcctIDFromPubKey == txSignerAcctID) { @@ -780,7 +867,7 @@ Transactor::checkMultiSign(PreclaimContext const& ctx) if (signerAccountFlags & lsfDisableMaster) { - JLOG(ctx.j.trace()) + JLOG(j.trace()) << "applyTransaction: Signer:Account lsfDisableMaster."; return tefMASTER_DISABLED; } @@ -792,21 +879,21 @@ Transactor::checkMultiSign(PreclaimContext const& ctx) // Public key must hash to the account's regular key. if (!sleTxSignerRoot) { - JLOG(ctx.j.trace()) << "applyTransaction: Non-phantom signer " - "lacks account root."; + JLOG(j.trace()) << "applyTransaction: Non-phantom signer " + "lacks account root."; return tefBAD_SIGNATURE; } if (!sleTxSignerRoot->isFieldPresent(sfRegularKey)) { - JLOG(ctx.j.trace()) + JLOG(j.trace()) << "applyTransaction: Account lacks RegularKey."; return tefBAD_SIGNATURE; } if (signingAcctIDFromPubKey != sleTxSignerRoot->getAccountID(sfRegularKey)) { - JLOG(ctx.j.trace()) + JLOG(j.trace()) << "applyTransaction: Account doesn't match RegularKey."; return tefBAD_SIGNATURE; } @@ -818,8 +905,7 @@ Transactor::checkMultiSign(PreclaimContext const& ctx) // Cannot perform transaction if quorum is not met. if (weightSum < sleAccountSigners->getFieldU32(sfSignerQuorum)) { - JLOG(ctx.j.trace()) - << "applyTransaction: Signers failed to meet quorum."; + JLOG(j.trace()) << "applyTransaction: Signers failed to meet quorum."; return tefBAD_QUORUM; } @@ -907,7 +993,11 @@ removeDeletedTrustLines( } } -/** Reset the context, discarding any changes made and adjust the fee */ +/** Reset the context, discarding any changes made and adjust the fee. + + @param fee The transaction fee to be charged. + @return A pair containing the transaction result and the actual fee charged. + */ std::pair Transactor::reset(XRPAmount fee) { @@ -915,9 +1005,10 @@ Transactor::reset(XRPAmount fee) auto const txnAcct = view().peek(keylet::account(ctx_.tx.getAccountID(sfAccount))); + + // The account should never be missing from the ledger. But if it + // is missing then we can't very well charge it a fee, can we? if (!txnAcct) - // The account should never be missing from the ledger. But if it - // is missing then we can't very well charge it a fee, can we? return {tefINTERNAL, beast::zero}; auto const payerSle = ctx_.tx.isFieldPresent(sfDelegate) @@ -1027,7 +1118,6 @@ Transactor::operator()() { // If the tapFAIL_HARD flag is set, a tec result // must not do anything - ctx_.discard(); applied = false; } diff --git a/src/xrpld/app/tx/detail/Transactor.h b/src/xrpld/app/tx/detail/Transactor.h index 88ccdb8db7..e94b93523d 100644 --- a/src/xrpld/app/tx/detail/Transactor.h +++ b/src/xrpld/app/tx/detail/Transactor.h @@ -38,14 +38,38 @@ public: STTx const& tx; Rules const rules; ApplyFlags flags; + std::optional parentBatchId; beast::Journal const j; + PreflightContext( + Application& app_, + STTx const& tx_, + uint256 parentBatchId_, + Rules const& rules_, + ApplyFlags flags_, + beast::Journal j_ = beast::Journal{beast::Journal::getNullSink()}) + : app(app_) + , tx(tx_) + , rules(rules_) + , flags(flags_) + , parentBatchId(parentBatchId_) + , j(j_) + { + XRPL_ASSERT( + (flags_ & tapBATCH) == tapBATCH, "Batch apply flag should be set"); + } + PreflightContext( Application& app_, STTx const& tx_, Rules const& rules_, ApplyFlags flags_, - beast::Journal j_); + beast::Journal j_ = beast::Journal{beast::Journal::getNullSink()}) + : app(app_), tx(tx_), rules(rules_), flags(flags_), j(j_) + { + XRPL_ASSERT( + (flags_ & tapBATCH) == 0, "Batch apply flag should not be set"); + } PreflightContext& operator=(PreflightContext const&) = delete; @@ -58,8 +82,9 @@ public: Application& app; ReadView const& view; TER preflightResult; - STTx const& tx; ApplyFlags flags; + STTx const& tx; + std::optional const parentBatchId; beast::Journal const j; PreclaimContext( @@ -68,14 +93,39 @@ public: TER preflightResult_, STTx const& tx_, ApplyFlags flags_, + std::optional parentBatchId_, beast::Journal j_ = beast::Journal{beast::Journal::getNullSink()}) : app(app_) , view(view_) , preflightResult(preflightResult_) - , tx(tx_) , flags(flags_) + , tx(tx_) + , parentBatchId(parentBatchId_) , j(j_) { + XRPL_ASSERT( + parentBatchId.has_value() == ((flags_ & tapBATCH) == tapBATCH), + "Parent Batch ID should be set if batch apply flag is set"); + } + + PreclaimContext( + Application& app_, + ReadView const& view_, + TER preflightResult_, + STTx const& tx_, + ApplyFlags flags_, + beast::Journal j_ = beast::Journal{beast::Journal::getNullSink()}) + : PreclaimContext( + app_, + view_, + preflightResult_, + tx_, + flags_, + std::nullopt, + j_) + { + XRPL_ASSERT( + (flags_ & tapBATCH) == 0, "Batch apply flag should not be set"); } PreclaimContext& @@ -141,6 +191,9 @@ public: static NotTEC checkSign(PreclaimContext const& ctx); + static NotTEC + checkBatchSign(PreclaimContext const& ctx); + // Returns the fee in fee units, not scaled for load. static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); @@ -202,9 +255,19 @@ private: TER payFee(); static NotTEC - checkSingleSign(PreclaimContext const& ctx); + checkSingleSign( + AccountID const& idSigner, + AccountID const& idAccount, + std::shared_ptr sleAccount, + Rules const& rules, + beast::Journal j); static NotTEC - checkMultiSign(PreclaimContext const& ctx); + checkMultiSign( + ReadView const& view, + AccountID const& idAccount, + STArray const& txSigners, + ApplyFlags const& flags, + beast::Journal j); void trapTransaction(uint256) const; }; diff --git a/src/xrpld/app/tx/detail/XChainBridge.cpp b/src/xrpld/app/tx/detail/XChainBridge.cpp index 5fa03557e5..6ca049ee66 100644 --- a/src/xrpld/app/tx/detail/XChainBridge.cpp +++ b/src/xrpld/app/tx/detail/XChainBridge.cpp @@ -511,6 +511,7 @@ transferHelper( /*offer crossing*/ OfferCrossing::no, /*limit quality*/ std::nullopt, /*sendmax*/ std::nullopt, + /*domain id*/ std::nullopt, j); if (auto const r = result.result(); diff --git a/src/xrpld/app/tx/detail/apply.cpp b/src/xrpld/app/tx/detail/apply.cpp index 615fd6a92d..889a520032 100644 --- a/src/xrpld/app/tx/detail/apply.cpp +++ b/src/xrpld/app/tx/detail/apply.cpp @@ -23,6 +23,7 @@ #include #include +#include namespace ripple { @@ -43,6 +44,28 @@ checkValidity( { auto const id = tx.getTransactionID(); auto const flags = router.getFlags(id); + + // Ignore signature check on batch inner transactions + if (tx.isFlag(tfInnerBatchTxn) && rules.enabled(featureBatch)) + { + // Defensive Check: These values are also checked in Batch::preflight + if (tx.isFieldPresent(sfTxnSignature) || + !tx.getSigningPubKey().empty() || tx.isFieldPresent(sfSigners)) + return { + Validity::SigBad, + "Malformed: Invalid inner batch transaction."}; + + std::string reason; + if (!passesLocalChecks(tx, reason)) + { + router.setFlags(id, SF_LOCALBAD); + return {Validity::SigGoodOnly, reason}; + } + + router.setFlags(id, SF_SIGGOOD); + return {Validity::Valid, ""}; + } + if (flags & SF_SIGBAD) // Signature is known bad return {Validity::SigBad, "Transaction has bad signature."}; @@ -106,6 +129,16 @@ forceValidity(HashRouter& router, uint256 const& txid, Validity validity) router.setFlags(txid, flags); } +template +ApplyResult +apply(Application& app, OpenView& view, PreflightChecks&& preflightChecks) +{ + STAmountSO stAmountSO{view.rules().enabled(fixSTAmountCanonicalize)}; + NumberSO stNumberSO{view.rules().enabled(fixUniversalNumber)}; + + return doApply(preclaim(preflightChecks(), app, view), app, view); +} + ApplyResult apply( Application& app, @@ -114,12 +147,89 @@ apply( ApplyFlags flags, beast::Journal j) { - STAmountSO stAmountSO{view.rules().enabled(fixSTAmountCanonicalize)}; - NumberSO stNumberSO{view.rules().enabled(fixUniversalNumber)}; + return apply(app, view, [&]() mutable { + return preflight(app, view.rules(), tx, flags, j); + }); +} - auto pfresult = preflight(app, view.rules(), tx, flags, j); - auto pcresult = preclaim(pfresult, app, view); - return doApply(pcresult, app, view); +ApplyResult +apply( + Application& app, + OpenView& view, + uint256 const& parentBatchId, + STTx const& tx, + ApplyFlags flags, + beast::Journal j) +{ + return apply(app, view, [&]() mutable { + return preflight(app, view.rules(), parentBatchId, tx, flags, j); + }); +} + +static bool +applyBatchTransactions( + Application& app, + OpenView& batchView, + STTx const& batchTxn, + beast::Journal j) +{ + XRPL_ASSERT( + batchTxn.getTxnType() == ttBATCH && + batchTxn.getFieldArray(sfRawTransactions).size() != 0, + "Batch transaction missing sfRawTransactions"); + + auto const parentBatchId = batchTxn.getTransactionID(); + auto const mode = batchTxn.getFlags(); + + auto applyOneTransaction = + [&app, &j, &parentBatchId, &batchView](STTx&& tx) { + OpenView perTxBatchView(batch_view, batchView); + + auto const ret = + apply(app, perTxBatchView, parentBatchId, tx, tapBATCH, j); + XRPL_ASSERT( + ret.applied == (isTesSuccess(ret.ter) || isTecClaim(ret.ter)), + "Inner transaction should not be applied"); + + JLOG(j.debug()) << "BatchTrace[" << parentBatchId + << "]: " << tx.getTransactionID() << " " + << (ret.applied ? "applied" : "failure") << ": " + << transToken(ret.ter); + + // If the transaction should be applied push its changes to the + // whole-batch view. + if (ret.applied && (isTesSuccess(ret.ter) || isTecClaim(ret.ter))) + perTxBatchView.apply(batchView); + + return ret; + }; + + int applied = 0; + + for (STObject rb : batchTxn.getFieldArray(sfRawTransactions)) + { + auto const result = applyOneTransaction(STTx{std::move(rb)}); + XRPL_ASSERT( + result.applied == + (isTesSuccess(result.ter) || isTecClaim(result.ter)), + "Outer Batch failure, inner transaction should not be applied"); + + if (result.applied) + ++applied; + + if (!isTesSuccess(result.ter)) + { + if (mode & tfAllOrNothing) + return false; + + if (mode & tfUntilFailure) + break; + } + else if (mode & tfOnlyOne) + break; + } + + return applied != 0; } ApplyTransactionResult @@ -141,10 +251,22 @@ applyTransaction( try { auto const result = apply(app, view, txn, flags, j); + if (result.applied) { JLOG(j.debug()) - << "Transaction applied: " << transHuman(result.ter); + << "Transaction applied: " << transToken(result.ter); + + // The batch transaction was just applied; now we need to apply + // its inner transactions as necessary. + if (isTesSuccess(result.ter) && txn.getTxnType() == ttBATCH) + { + OpenView wholeBatchView(batch_view, view); + + if (applyBatchTransactions(app, wholeBatchView, txn, j)) + wholeBatchView.apply(view); + } + return ApplyTransactionResult::Success; } diff --git a/src/xrpld/app/tx/detail/applySteps.cpp b/src/xrpld/app/tx/detail/applySteps.cpp index 32745f703d..543bedcd47 100644 --- a/src/xrpld/app/tx/detail/applySteps.cpp +++ b/src/xrpld/app/tx/detail/applySteps.cpp @@ -68,7 +68,6 @@ with_txn_type(TxType txnType, F&& f) #undef TRANSACTION #pragma pop_macro("TRANSACTION") - default: throw UnknownTxnType(txnType); } @@ -191,6 +190,22 @@ invoke_preclaim(PreclaimContext const& ctx) } } +/** + * @brief Calculates the base fee for a given transaction. + * + * This function determines the base fee required for the specified transaction + * by invoking the appropriate fee calculation logic based on the transaction + * type. It uses a type-dispatch mechanism to select the correct calculation + * method. + * + * @param view The ledger view to use for fee calculation. + * @param tx The transaction for which the base fee is to be calculated. + * @return The calculated base fee as an XRPAmount. + * + * @throws std::exception If an error occurs during fee calculation, including + * but not limited to unknown transaction types or internal errors, the function + * logs an error and returns an XRPAmount of zero. + */ static XRPAmount invoke_calculateBaseFee(ReadView const& view, STTx const& tx) { @@ -284,7 +299,28 @@ preflight( } catch (std::exception const& e) { - JLOG(j.fatal()) << "apply: " << e.what(); + JLOG(j.fatal()) << "apply (preflight): " << e.what(); + return {pfctx, {tefEXCEPTION, TxConsequences{tx}}}; + } +} + +PreflightResult +preflight( + Application& app, + Rules const& rules, + uint256 const& parentBatchId, + STTx const& tx, + ApplyFlags flags, + beast::Journal j) +{ + PreflightContext const pfctx(app, tx, parentBatchId, rules, flags, j); + try + { + return {pfctx, invoke_preflight(pfctx)}; + } + catch (std::exception const& e) + { + JLOG(j.fatal()) << "apply (preflight): " << e.what(); return {pfctx, {tefEXCEPTION, TxConsequences{tx}}}; } } @@ -298,18 +334,31 @@ preclaim( std::optional ctx; if (preflightResult.rules != view.rules()) { - auto secondFlight = preflight( - app, - view.rules(), - preflightResult.tx, - preflightResult.flags, - preflightResult.j); + auto secondFlight = [&]() { + if (preflightResult.parentBatchId) + return preflight( + app, + view.rules(), + preflightResult.parentBatchId.value(), + preflightResult.tx, + preflightResult.flags, + preflightResult.j); + + return preflight( + app, + view.rules(), + preflightResult.tx, + preflightResult.flags, + preflightResult.j); + }(); + ctx.emplace( app, view, secondFlight.ter, secondFlight.tx, secondFlight.flags, + secondFlight.parentBatchId, secondFlight.j); } else @@ -320,8 +369,10 @@ preclaim( preflightResult.ter, preflightResult.tx, preflightResult.flags, + preflightResult.parentBatchId, preflightResult.j); } + try { if (ctx->preflightResult != tesSUCCESS) @@ -330,7 +381,7 @@ preclaim( } catch (std::exception const& e) { - JLOG(ctx->j.fatal()) << "apply: " << e.what(); + JLOG(ctx->j.fatal()) << "apply (preclaim): " << e.what(); return {*ctx, tefEXCEPTION}; } } @@ -363,6 +414,7 @@ doApply(PreclaimResult const& preclaimResult, Application& app, OpenView& view) ApplyContext ctx( app, view, + preclaimResult.parentBatchId, preclaimResult.tx, preclaimResult.ter, calculateBaseFee(view, preclaimResult.tx), diff --git a/src/xrpld/core/Config.h b/src/xrpld/core/Config.h index 4fdce92c8a..a58867958b 100644 --- a/src/xrpld/core/Config.h +++ b/src/xrpld/core/Config.h @@ -242,19 +242,18 @@ public: // size, but we allow admins to explicitly set it in the config. std::optional SWEEP_INTERVAL; - // Reduce-relay - these parameters are experimental. - // Enable reduce-relay features - // Validation/proposal reduce-relay feature - bool VP_REDUCE_RELAY_ENABLE = false; - // Send squelch message to peers. Generally this config should - // have the same value as VP_REDUCE_RELAY_ENABLE. It can be - // used for testing the feature's function without - // affecting the message relaying. To use it for testing, - // set it to false and set VP_REDUCE_RELAY_ENABLE to true. - // Squelch messages will not be sent to the peers in this case. - // Set log level to debug so that the feature function can be - // analyzed. - bool VP_REDUCE_RELAY_SQUELCH = false; + // Reduce-relay - Experimental parameters to control p2p routing algorithms + + // Enable base squelching of duplicate validation/proposal messages + bool VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE = false; + + ///////////////////// !!TEMPORARY CODE BLOCK!! //////////////////////// + // Temporary squelching config for the peers selected as a source of // + // validator messages. The config must be removed once squelching is // + // made the default routing algorithm // + std::size_t VP_REDUCE_RELAY_SQUELCH_MAX_SELECTED_PEERS = 5; + ///////////////// END OF TEMPORARY CODE BLOCK ///////////////////// + // Transaction reduce-relay feature bool TX_REDUCE_RELAY_ENABLE = false; // If tx reduce-relay feature is disabled diff --git a/src/xrpld/core/JobQueue.h b/src/xrpld/core/JobQueue.h index 051c298251..eda956c019 100644 --- a/src/xrpld/core/JobQueue.h +++ b/src/xrpld/core/JobQueue.h @@ -30,6 +30,8 @@ #include +#include + namespace ripple { namespace perf { diff --git a/src/xrpld/core/detail/Config.cpp b/src/xrpld/core/detail/Config.cpp index b132987d08..1a07109b74 100644 --- a/src/xrpld/core/detail/Config.cpp +++ b/src/xrpld/core/detail/Config.cpp @@ -737,8 +737,44 @@ Config::loadFromString(std::string const& fileContents) if (exists(SECTION_REDUCE_RELAY)) { auto sec = section(SECTION_REDUCE_RELAY); - VP_REDUCE_RELAY_ENABLE = sec.value_or("vp_enable", false); - VP_REDUCE_RELAY_SQUELCH = sec.value_or("vp_squelch", false); + + ///////////////////// !!TEMPORARY CODE BLOCK!! //////////////////////// + // vp_enable config option is deprecated by vp_base_squelch_enable // + // This option is kept for backwards compatibility. When squelching // + // is the default algorithm, it must be replaced with: // + // VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE = // + // sec.value_or("vp_base_squelch_enable", true); // + if (sec.exists("vp_base_squelch_enable") && sec.exists("vp_enable")) + Throw( + "Invalid " SECTION_REDUCE_RELAY + " cannot specify both vp_base_squelch_enable and vp_enable " + "options. " + "vp_enable was deprecated and replaced by " + "vp_base_squelch_enable"); + + if (sec.exists("vp_base_squelch_enable")) + VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE = + sec.value_or("vp_base_squelch_enable", false); + else if (sec.exists("vp_enable")) + VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE = + sec.value_or("vp_enable", false); + else + VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE = false; + ///////////////// !!END OF TEMPORARY CODE BLOCK!! ///////////////////// + + ///////////////////// !!TEMPORARY CODE BLOCK!! /////////////////////// + // Temporary squelching config for the peers selected as a source of // + // validator messages. The config must be removed once squelching is // + // made the default routing algorithm. // + VP_REDUCE_RELAY_SQUELCH_MAX_SELECTED_PEERS = + sec.value_or("vp_base_squelch_max_selected_peers", 5); + if (VP_REDUCE_RELAY_SQUELCH_MAX_SELECTED_PEERS < 3) + Throw( + "Invalid " SECTION_REDUCE_RELAY + " vp_base_squelch_max_selected_peers must be " + "greater than or equal to 3"); + ///////////////// !!END OF TEMPORARY CODE BLOCK!! ///////////////////// + TX_REDUCE_RELAY_ENABLE = sec.value_or("tx_enable", false); TX_REDUCE_RELAY_METRICS = sec.value_or("tx_metrics", false); TX_REDUCE_RELAY_MIN_PEERS = sec.value_or("tx_min_peers", 20); @@ -747,9 +783,9 @@ Config::loadFromString(std::string const& fileContents) TX_REDUCE_RELAY_MIN_PEERS < 10) Throw( "Invalid " SECTION_REDUCE_RELAY - ", tx_min_peers must be greater or equal to 10" - ", tx_relay_percentage must be greater or equal to 10 " - "and less or equal to 100"); + ", tx_min_peers must be greater than or equal to 10" + ", tx_relay_percentage must be greater than or equal to 10 " + "and less than or equal to 100"); } if (getSingleSection(secConfig, SECTION_MAX_TRANSACTIONS, strTemp, j_)) diff --git a/src/xrpld/ledger/ApplyView.h b/src/xrpld/ledger/ApplyView.h index 1abff33be0..1e4a5a112a 100644 --- a/src/xrpld/ledger/ApplyView.h +++ b/src/xrpld/ledger/ApplyView.h @@ -42,6 +42,9 @@ enum ApplyFlags : std::uint32_t { // Transaction came from a privileged source tapUNLIMITED = 0x400, + // Transaction is executing as part of a batch + tapBATCH = 0x800, + // Transaction shouldn't be applied // Signatures shouldn't be checked tapDRY_RUN = 0x1000 diff --git a/src/xrpld/ledger/ApplyViewImpl.h b/src/xrpld/ledger/ApplyViewImpl.h index 1c282565b1..d170cf71ff 100644 --- a/src/xrpld/ledger/ApplyViewImpl.h +++ b/src/xrpld/ledger/ApplyViewImpl.h @@ -58,6 +58,7 @@ public: OpenView& to, STTx const& tx, TER ter, + std::optional parentBatchId, bool isDryRun, beast::Journal j); diff --git a/src/xrpld/ledger/OpenView.h b/src/xrpld/ledger/OpenView.h index ecc618e185..a1fa195a69 100644 --- a/src/xrpld/ledger/OpenView.h +++ b/src/xrpld/ledger/OpenView.h @@ -24,6 +24,7 @@ #include #include +#include #include #include @@ -39,13 +40,21 @@ namespace ripple { Views constructed with this tag will have the rules of open ledgers applied during transaction processing. -*/ -struct open_ledger_t + */ +inline constexpr struct open_ledger_t { - explicit open_ledger_t() = default; -}; + explicit constexpr open_ledger_t() = default; +} open_ledger{}; -extern open_ledger_t const open_ledger; +/** Batch view construction tag. + + Views constructed with this tag are part of a stack of views + used during batch transaction applied. + */ +inline constexpr struct batch_view_t +{ + explicit constexpr batch_view_t() = default; +} batch_view{}; //------------------------------------------------------------------------------ @@ -97,6 +106,10 @@ private: ReadView const* base_; detail::RawStateTable items_; std::shared_ptr hold_; + + /// In batch mode, the number of transactions already executed. + std::size_t baseTxCount_ = 0; + bool open_ = true; public: @@ -142,7 +155,6 @@ public: The tx list starts empty and will contain all newly inserted tx. */ - /** @{ */ OpenView( open_ledger_t, ReadView const* base, @@ -156,7 +168,11 @@ public: : OpenView(open_ledger, &*base, rules, base) { } - /** @} */ + + OpenView(batch_view_t, OpenView& base) : OpenView(std::addressof(base)) + { + baseTxCount_ = base.txCount(); + } /** Construct a new last closed ledger. diff --git a/src/xrpld/ledger/PaymentSandbox.h b/src/xrpld/ledger/PaymentSandbox.h index a41a0211a2..2cd31ea490 100644 --- a/src/xrpld/ledger/PaymentSandbox.h +++ b/src/xrpld/ledger/PaymentSandbox.h @@ -27,7 +27,6 @@ #include #include -#include namespace ripple { diff --git a/src/xrpld/ledger/View.h b/src/xrpld/ledger/View.h index 5aa8b7216d..6bfeb6a6da 100644 --- a/src/xrpld/ledger/View.h +++ b/src/xrpld/ledger/View.h @@ -357,6 +357,13 @@ transferRate(ReadView const& view, AccountID const& issuer); [[nodiscard]] Rate transferRate(ReadView const& view, MPTID const& issuanceID); +/** Returns the transfer fee as Rate based on the type of token + * @param view The ledger view + * @param amount The amount to transfer + */ +[[nodiscard]] Rate +transferRate(ReadView const& view, STAmount const& amount); + /** Returns `true` if the directory is empty @param key The key of the directory */ @@ -683,6 +690,21 @@ rippleCredit( bool bCheckIssuer, beast::Journal j); +TER +rippleLockEscrowMPT( + ApplyView& view, + AccountID const& uGrantorID, + STAmount const& saAmount, + beast::Journal j); + +TER +rippleUnlockEscrowMPT( + ApplyView& view, + AccountID const& uGrantorID, + AccountID const& uGranteeID, + STAmount const& saAmount, + beast::Journal j); + /** Calls static accountSendIOU if saAmount represents Issue. * Calls static accountSendMPT if saAmount represents MPTIssue. */ diff --git a/src/xrpld/ledger/detail/ApplyStateTable.cpp b/src/xrpld/ledger/detail/ApplyStateTable.cpp index c11a72d782..2a740093d9 100644 --- a/src/xrpld/ledger/detail/ApplyStateTable.cpp +++ b/src/xrpld/ledger/detail/ApplyStateTable.cpp @@ -116,6 +116,7 @@ ApplyStateTable::apply( STTx const& tx, TER ter, std::optional const& deliver, + std::optional const& parentBatchId, bool isDryRun, beast::Journal j) { @@ -126,9 +127,11 @@ ApplyStateTable::apply( std::optional metadata; if (!to.open() || isDryRun) { - TxMeta meta(tx.getTransactionID(), to.seq()); + TxMeta meta(tx.getTransactionID(), to.seq(), parentBatchId); + if (deliver) meta.setDeliveredAmount(*deliver); + Mods newMod; for (auto& item : items_) { diff --git a/src/xrpld/ledger/detail/ApplyStateTable.h b/src/xrpld/ledger/detail/ApplyStateTable.h index b1bac733fc..5a2e0bcf54 100644 --- a/src/xrpld/ledger/detail/ApplyStateTable.h +++ b/src/xrpld/ledger/detail/ApplyStateTable.h @@ -72,6 +72,7 @@ public: STTx const& tx, TER ter, std::optional const& deliver, + std::optional const& parentBatchId, bool isDryRun, beast::Journal j); diff --git a/src/xrpld/ledger/detail/ApplyViewImpl.cpp b/src/xrpld/ledger/detail/ApplyViewImpl.cpp index 74b71c8324..3fd9478b54 100644 --- a/src/xrpld/ledger/detail/ApplyViewImpl.cpp +++ b/src/xrpld/ledger/detail/ApplyViewImpl.cpp @@ -31,10 +31,11 @@ ApplyViewImpl::apply( OpenView& to, STTx const& tx, TER ter, + std::optional parentBatchId, bool isDryRun, beast::Journal j) { - return items_.apply(to, tx, ter, deliver_, isDryRun, j); + return items_.apply(to, tx, ter, deliver_, parentBatchId, isDryRun, j); } std::size_t diff --git a/src/xrpld/ledger/detail/OpenView.cpp b/src/xrpld/ledger/detail/OpenView.cpp index 5c62d8cef8..73e502a5e2 100644 --- a/src/xrpld/ledger/detail/OpenView.cpp +++ b/src/xrpld/ledger/detail/OpenView.cpp @@ -23,8 +23,6 @@ namespace ripple { -open_ledger_t const open_ledger{}; - class OpenView::txs_iter_impl : public txs_type::iter_base { private: @@ -124,7 +122,7 @@ OpenView::OpenView(ReadView const* base, std::shared_ptr hold) std::size_t OpenView::txCount() const { - return txs_.size(); + return baseTxCount_ + txs_.size(); } void @@ -269,7 +267,7 @@ OpenView::rawTxInsert( std::forward_as_tuple(key), std::forward_as_tuple(txn, metaData)); if (!result.second) - LogicError("rawTxInsert: duplicate TX id" + to_string(key)); + LogicError("rawTxInsert: duplicate TX id: " + to_string(key)); } } // namespace ripple diff --git a/src/xrpld/ledger/detail/View.cpp b/src/xrpld/ledger/detail/View.cpp index 077b1a172a..2296a159b7 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/xrpld/ledger/detail/View.cpp @@ -37,7 +37,6 @@ #include #include -#include #include #include @@ -782,6 +781,19 @@ transferRate(ReadView const& view, MPTID const& issuanceID) return parityRate; } +Rate +transferRate(ReadView const& view, STAmount const& amount) +{ + return std::visit( + [&](TIss const& issue) { + if constexpr (std::is_same_v) + return transferRate(view, issue.getIssuer()); + else + return transferRate(view, issue.getMptID()); + }, + amount.asset().value()); +} + bool areCompatible( ReadView const& validLedger, @@ -1056,8 +1068,8 @@ AccountID pseudoAccountAddress(ReadView const& view, uint256 const& pseudoOwnerKey) { // This number must not be changed without an amendment - constexpr int maxAccountAttempts = 256; - for (auto i = 0; i < maxAccountAttempts; ++i) + constexpr std::uint16_t maxAccountAttempts = 256; + for (std::uint16_t i = 0; i < maxAccountAttempts; ++i) { ripesha_hasher rsh; auto const hash = sha512Half(i, view.info().parentHash, pseudoOwnerKey); @@ -1541,6 +1553,27 @@ offerDelete(ApplyView& view, std::shared_ptr const& sle, beast::Journal j) return tefBAD_LEDGER; } + if (sle->isFieldPresent(sfAdditionalBooks)) + { + XRPL_ASSERT( + sle->isFlag(lsfHybrid) && sle->isFieldPresent(sfDomainID), + "ripple::offerDelete : should be a hybrid domain offer"); + + auto const& additionalBookDirs = sle->getFieldArray(sfAdditionalBooks); + + for (auto const& bookDir : additionalBookDirs) + { + auto const& dirIndex = bookDir.getFieldH256(sfBookDirectory); + auto const& dirNode = bookDir.getFieldU64(sfBookNode); + + if (!view.dirRemove( + keylet::page(dirIndex), dirNode, offerIndex, false)) + { + return tefBAD_LEDGER; // LCOV_EXCL_LINE + } + } + } + adjustOwnerCount(view, view.peek(keylet::account(owner)), -1, j); view.erase(sle); @@ -2448,8 +2481,19 @@ enforceMPTokenAuthorization( auto const keylet = keylet::mptoken(mptIssuanceID, account); auto const sleToken = view.read(keylet); // NOTE: might be null auto const maybeDomainID = sleIssuance->at(~sfDomainID); - bool const authorizedByDomain = maybeDomainID.has_value() && - verifyValidDomain(view, account, *maybeDomainID, j) == tesSUCCESS; + bool expired = false; + bool const authorizedByDomain = [&]() -> bool { + // NOTE: defensive here, shuld be checked in preclaim + if (!maybeDomainID.has_value()) + return false; // LCOV_EXCL_LINE + + auto const ter = verifyValidDomain(view, account, *maybeDomainID, j); + if (isTesSuccess(ter)) + return true; + if (ter == tecEXPIRED) + expired = true; + return false; + }(); if (!authorizedByDomain && sleToken == nullptr) { @@ -2460,14 +2504,14 @@ enforceMPTokenAuthorization( // 3. Account has all expired credentials (deleted in verifyValidDomain) // // Either way, return tecNO_AUTH and there is nothing else to do - return tecNO_AUTH; + return expired ? tecEXPIRED : tecNO_AUTH; } else if (!authorizedByDomain && maybeDomainID.has_value()) { // Found an MPToken but the account is not authorized and we expect // it to have been authorized by the domain. This could be because the // credentials used to create the MPToken have expired or been deleted. - return tecNO_AUTH; + return expired ? tecEXPIRED : tecNO_AUTH; } else if (!authorizedByDomain) { @@ -2749,6 +2793,250 @@ sharesToAssetsWithdraw( return assets; } +TER +rippleLockEscrowMPT( + ApplyView& view, + AccountID const& sender, + STAmount const& amount, + beast::Journal j) +{ + auto const mptIssue = amount.get(); + auto const mptID = keylet::mptIssuance(mptIssue.getMptID()); + auto sleIssuance = view.peek(mptID); + if (!sleIssuance) + { // LCOV_EXCL_START + JLOG(j.error()) << "rippleLockEscrowMPT: MPT issuance not found for " + << mptIssue.getMptID(); + return tecOBJECT_NOT_FOUND; + } // LCOV_EXCL_STOP + + if (amount.getIssuer() == sender) + { // LCOV_EXCL_START + JLOG(j.error()) + << "rippleLockEscrowMPT: sender is the issuer, cannot lock MPTs."; + return tecINTERNAL; + } // LCOV_EXCL_STOP + + // 1. Decrease the MPT Holder MPTAmount + // 2. Increase the MPT Holder EscrowedAmount + { + auto const mptokenID = keylet::mptoken(mptID.key, sender); + auto sle = view.peek(mptokenID); + if (!sle) + { // LCOV_EXCL_START + JLOG(j.error()) + << "rippleLockEscrowMPT: MPToken not found for " << sender; + return tecOBJECT_NOT_FOUND; + } // LCOV_EXCL_STOP + + auto const amt = sle->getFieldU64(sfMPTAmount); + auto const pay = amount.mpt().value(); + + // Underflow check for subtraction + if (!canSubtract(STAmount(mptIssue, amt), STAmount(mptIssue, pay))) + { // LCOV_EXCL_START + JLOG(j.error()) + << "rippleLockEscrowMPT: insufficient MPTAmount for " + << to_string(sender) << ": " << amt << " < " << pay; + return tecINTERNAL; + } // LCOV_EXCL_STOP + + (*sle)[sfMPTAmount] = amt - pay; + + // Overflow check for addition + uint64_t const locked = (*sle)[~sfLockedAmount].value_or(0); + + if (!canAdd(STAmount(mptIssue, locked), STAmount(mptIssue, pay))) + { // LCOV_EXCL_START + JLOG(j.error()) + << "rippleLockEscrowMPT: overflow on locked amount for " + << to_string(sender) << ": " << locked << " + " << pay; + return tecINTERNAL; + } // LCOV_EXCL_STOP + + if (sle->isFieldPresent(sfLockedAmount)) + (*sle)[sfLockedAmount] += pay; + else + sle->setFieldU64(sfLockedAmount, pay); + + view.update(sle); + } + + // 1. Increase the Issuance EscrowedAmount + // 2. DO NOT change the Issuance OutstandingAmount + { + uint64_t const issuanceEscrowed = + (*sleIssuance)[~sfLockedAmount].value_or(0); + auto const pay = amount.mpt().value(); + + // Overflow check for addition + if (!canAdd( + STAmount(mptIssue, issuanceEscrowed), STAmount(mptIssue, pay))) + { // LCOV_EXCL_START + JLOG(j.error()) << "rippleLockEscrowMPT: overflow on issuance " + "locked amount for " + << mptIssue.getMptID() << ": " << issuanceEscrowed + << " + " << pay; + return tecINTERNAL; + } // LCOV_EXCL_STOP + + if (sleIssuance->isFieldPresent(sfLockedAmount)) + (*sleIssuance)[sfLockedAmount] += pay; + else + sleIssuance->setFieldU64(sfLockedAmount, pay); + + view.update(sleIssuance); + } + return tesSUCCESS; +} + +TER +rippleUnlockEscrowMPT( + ApplyView& view, + AccountID const& sender, + AccountID const& receiver, + STAmount const& amount, + beast::Journal j) +{ + auto const issuer = amount.getIssuer(); + auto const mptIssue = amount.get(); + auto const mptID = keylet::mptIssuance(mptIssue.getMptID()); + auto sleIssuance = view.peek(mptID); + if (!sleIssuance) + { // LCOV_EXCL_START + JLOG(j.error()) << "rippleUnlockEscrowMPT: MPT issuance not found for " + << mptIssue.getMptID(); + return tecOBJECT_NOT_FOUND; + } // LCOV_EXCL_STOP + + // Decrease the Issuance EscrowedAmount + { + if (!sleIssuance->isFieldPresent(sfLockedAmount)) + { // LCOV_EXCL_START + JLOG(j.error()) + << "rippleUnlockEscrowMPT: no locked amount in issuance for " + << mptIssue.getMptID(); + return tecINTERNAL; + } // LCOV_EXCL_STOP + + auto const locked = sleIssuance->getFieldU64(sfLockedAmount); + auto const redeem = amount.mpt().value(); + + // Underflow check for subtraction + if (!canSubtract( + STAmount(mptIssue, locked), STAmount(mptIssue, redeem))) + { // LCOV_EXCL_START + JLOG(j.error()) + << "rippleUnlockEscrowMPT: insufficient locked amount for " + << mptIssue.getMptID() << ": " << locked << " < " << redeem; + return tecINTERNAL; + } // LCOV_EXCL_STOP + + auto const newLocked = locked - redeem; + if (newLocked == 0) + sleIssuance->makeFieldAbsent(sfLockedAmount); + else + sleIssuance->setFieldU64(sfLockedAmount, newLocked); + view.update(sleIssuance); + } + + if (issuer != receiver) + { + // Increase the MPT Holder MPTAmount + auto const mptokenID = keylet::mptoken(mptID.key, receiver); + auto sle = view.peek(mptokenID); + if (!sle) + { // LCOV_EXCL_START + JLOG(j.error()) + << "rippleUnlockEscrowMPT: MPToken not found for " << receiver; + return tecOBJECT_NOT_FOUND; // LCOV_EXCL_LINE + } // LCOV_EXCL_STOP + + auto current = sle->getFieldU64(sfMPTAmount); + auto delta = amount.mpt().value(); + + // Overflow check for addition + if (!canAdd(STAmount(mptIssue, current), STAmount(mptIssue, delta))) + { // LCOV_EXCL_START + JLOG(j.error()) + << "rippleUnlockEscrowMPT: overflow on MPTAmount for " + << to_string(receiver) << ": " << current << " + " << delta; + return tecINTERNAL; + } // LCOV_EXCL_STOP + + (*sle)[sfMPTAmount] += delta; + view.update(sle); + } + else + { + // Decrease the Issuance OutstandingAmount + auto const outstanding = sleIssuance->getFieldU64(sfOutstandingAmount); + auto const redeem = amount.mpt().value(); + + // Underflow check for subtraction + if (!canSubtract( + STAmount(mptIssue, outstanding), STAmount(mptIssue, redeem))) + { // LCOV_EXCL_START + JLOG(j.error()) + << "rippleUnlockEscrowMPT: insufficient outstanding amount for " + << mptIssue.getMptID() << ": " << outstanding << " < " + << redeem; + return tecINTERNAL; + } // LCOV_EXCL_STOP + + sleIssuance->setFieldU64(sfOutstandingAmount, outstanding - redeem); + view.update(sleIssuance); + } + + if (issuer == sender) + { // LCOV_EXCL_START + JLOG(j.error()) << "rippleUnlockEscrowMPT: sender is the issuer, " + "cannot unlock MPTs."; + return tecINTERNAL; + } // LCOV_EXCL_STOP + else + { + // Decrease the MPT Holder EscrowedAmount + auto const mptokenID = keylet::mptoken(mptID.key, sender); + auto sle = view.peek(mptokenID); + if (!sle) + { // LCOV_EXCL_START + JLOG(j.error()) + << "rippleUnlockEscrowMPT: MPToken not found for " << sender; + return tecOBJECT_NOT_FOUND; + } // LCOV_EXCL_STOP + + if (!sle->isFieldPresent(sfLockedAmount)) + { // LCOV_EXCL_START + JLOG(j.error()) + << "rippleUnlockEscrowMPT: no locked amount in MPToken for " + << to_string(sender); + return tecINTERNAL; + } // LCOV_EXCL_STOP + + auto const locked = sle->getFieldU64(sfLockedAmount); + auto const delta = amount.mpt().value(); + + // Underflow check for subtraction + // LCOV_EXCL_START + if (!canSubtract(STAmount(mptIssue, locked), STAmount(mptIssue, delta))) + { // LCOV_EXCL_START + JLOG(j.error()) + << "rippleUnlockEscrowMPT: insufficient locked amount for " + << to_string(sender) << ": " << locked << " < " << delta; + return tecINTERNAL; + } // LCOV_EXCL_STOP + + auto const newLocked = locked - delta; + if (newLocked == 0) + sle->makeFieldAbsent(sfLockedAmount); + else + sle->setFieldU64(sfLockedAmount, newLocked); + view.update(sle); + } + return tesSUCCESS; +} + bool after(NetClock::time_point now, std::uint32_t mark) { diff --git a/src/xrpld/overlay/Slot.h b/src/xrpld/overlay/Slot.h index 6ae3c9a142..b2d03164aa 100644 --- a/src/xrpld/overlay/Slot.h +++ b/src/xrpld/overlay/Slot.h @@ -20,6 +20,7 @@ #ifndef RIPPLE_OVERLAY_SLOT_H_INCLUDED #define RIPPLE_OVERLAY_SLOT_H_INCLUDED +#include #include #include @@ -32,7 +33,6 @@ #include #include -#include #include #include #include @@ -109,16 +109,25 @@ private: using id_t = Peer::id_t; using time_point = typename clock_type::time_point; + // a callback to report ignored squelches + using ignored_squelch_callback = std::function; + /** Constructor * @param journal Journal for logging * @param handler Squelch/Unsquelch implementation + * @param maxSelectedPeers the maximum number of peers to be selected as + * validator message source */ - Slot(SquelchHandler const& handler, beast::Journal journal) + Slot( + SquelchHandler const& handler, + beast::Journal journal, + uint16_t maxSelectedPeers) : reachedThreshold_(0) , lastSelected_(clock_type::now()) , state_(SlotState::Counting) , handler_(handler) , journal_(journal) + , maxSelectedPeers_(maxSelectedPeers) { } @@ -129,7 +138,7 @@ private: * slot's state to Counting. If the number of messages for the peer is > * MIN_MESSAGE_THRESHOLD then add peer to considered peers pool. If the * number of considered peers who reached MAX_MESSAGE_THRESHOLD is - * MAX_SELECTED_PEERS then randomly select MAX_SELECTED_PEERS from + * maxSelectedPeers_ then randomly select maxSelectedPeers_ from * considered peers, and call squelch handler for each peer, which is not * selected and not already in Squelched state. Set the state for those * peers to Squelched and reset the count of all peers. Set slot's state to @@ -139,9 +148,14 @@ private: * @param id Peer id which received the message * @param type Message type (Validation and Propose Set only, * others are ignored, future use) + * @param callback A callback to report ignored squelches */ void - update(PublicKey const& validator, id_t id, protocol::MessageType type); + update( + PublicKey const& validator, + id_t id, + protocol::MessageType type, + ignored_squelch_callback callback); /** Handle peer deletion when a peer disconnects. * If the peer is in Selected state then @@ -223,17 +237,26 @@ private: time_point expire; // squelch expiration time time_point lastMessage; // time last message received }; + std::unordered_map peers_; // peer's data + // pool of peers considered as the source of messages // from validator - peers that reached MIN_MESSAGE_THRESHOLD std::unordered_set considered_; + // number of peers that reached MAX_MESSAGE_THRESHOLD std::uint16_t reachedThreshold_; + // last time peers were selected, used to age the slot typename clock_type::time_point lastSelected_; + SlotState state_; // slot's state SquelchHandler const& handler_; // squelch/unsquelch handler beast::Journal const journal_; // logging + + // the maximum number of peers that should be selected as a validator + // message source + uint16_t const maxSelectedPeers_; }; template @@ -264,7 +287,8 @@ void Slot::update( PublicKey const& validator, id_t id, - protocol::MessageType type) + protocol::MessageType type, + ignored_squelch_callback callback) { using namespace std::chrono; auto now = clock_type::now(); @@ -302,6 +326,10 @@ Slot::update( peer.lastMessage = now; + // report if we received a message from a squelched peer + if (peer.state == PeerState::Squelched) + callback(); + if (state_ != SlotState::Counting || peer.state == PeerState::Squelched) return; @@ -319,17 +347,17 @@ Slot::update( return; } - if (reachedThreshold_ == MAX_SELECTED_PEERS) + if (reachedThreshold_ == maxSelectedPeers_) { - // Randomly select MAX_SELECTED_PEERS peers from considered. + // Randomly select maxSelectedPeers_ peers from considered. // Exclude peers that have been idling > IDLED - // it's possible that deleteIdlePeer() has not been called yet. - // If number of remaining peers != MAX_SELECTED_PEERS + // If number of remaining peers != maxSelectedPeers_ // then reset the Counting state and let deleteIdlePeer() handle // idled peers. std::unordered_set selected; auto const consideredPoolSize = considered_.size(); - while (selected.size() != MAX_SELECTED_PEERS && considered_.size() != 0) + while (selected.size() != maxSelectedPeers_ && considered_.size() != 0) { auto i = considered_.size() == 1 ? 0 : rand_int(considered_.size() - 1); @@ -347,7 +375,7 @@ Slot::update( selected.insert(id); } - if (selected.size() != MAX_SELECTED_PEERS) + if (selected.size() != maxSelectedPeers_) { JLOG(journal_.trace()) << "update: selection failed " << Slice(validator) << " " << id; @@ -364,7 +392,7 @@ Slot::update( << *std::next(s, 1) << " " << *std::next(s, 2); XRPL_ASSERT( - peers_.size() >= MAX_SELECTED_PEERS, + peers_.size() >= maxSelectedPeers_, "ripple::reduce_relay::Slot::update : minimum peers"); // squelch peers which are not selected and @@ -382,7 +410,7 @@ Slot::update( str << k << " "; v.state = PeerState::Squelched; std::chrono::seconds duration = - getSquelchDuration(peers_.size() - MAX_SELECTED_PEERS); + getSquelchDuration(peers_.size() - maxSelectedPeers_); v.expire = now + duration; handler_.squelch(validator, k, duration.count()); } @@ -544,15 +572,41 @@ class Slots final public: /** - * @param app Applicaton reference + * @param logs reference to the logger * @param handler Squelch/unsquelch implementation + * @param config reference to the global config */ - Slots(Logs& logs, SquelchHandler const& handler) - : handler_(handler), logs_(logs), journal_(logs.journal("Slots")) + Slots(Logs& logs, SquelchHandler const& handler, Config const& config) + : handler_(handler) + , logs_(logs) + , journal_(logs.journal("Slots")) + , baseSquelchEnabled_(config.VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE) + , maxSelectedPeers_(config.VP_REDUCE_RELAY_SQUELCH_MAX_SELECTED_PEERS) { } ~Slots() = default; - /** Calls Slot::update of Slot associated with the validator. + + /** Check if base squelching feature is enabled and ready */ + bool + baseSquelchReady() + { + return baseSquelchEnabled_ && reduceRelayReady(); + } + + /** Check if reduce_relay::WAIT_ON_BOOTUP time passed since startup */ + bool + reduceRelayReady() + { + if (!reduceRelayReady_) + reduceRelayReady_ = + reduce_relay::epoch(clock_type::now()) > + reduce_relay::WAIT_ON_BOOTUP; + + return reduceRelayReady_; + } + + /** Calls Slot::update of Slot associated with the validator, with a noop + * callback. * @param key Message's hash * @param validator Validator's public key * @param id Peer's id which received the message @@ -563,7 +617,25 @@ public: uint256 const& key, PublicKey const& validator, id_t id, - protocol::MessageType type); + protocol::MessageType type) + { + updateSlotAndSquelch(key, validator, id, type, []() {}); + } + + /** Calls Slot::update of Slot associated with the validator. + * @param key Message's hash + * @param validator Validator's public key + * @param id Peer's id which received the message + * @param type Received protocol message type + * @param callback A callback to report ignored validations + */ + void + updateSlotAndSquelch( + uint256 const& key, + PublicKey const& validator, + id_t id, + protocol::MessageType type, + typename Slot::ignored_squelch_callback callback); /** Check if peers stopped relaying messages * and if slots stopped receiving messages from the validator. @@ -651,10 +723,16 @@ private: bool addPeerMessage(uint256 const& key, id_t id); + std::atomic_bool reduceRelayReady_{false}; + hash_map> slots_; SquelchHandler const& handler_; // squelch/unsquelch handler Logs& logs_; beast::Journal const journal_; + + bool const baseSquelchEnabled_; + uint16_t const maxSelectedPeers_; + // Maintain aged container of message/peers. This is required // to discard duplicate message from the same peer. A message // is aged after IDLED seconds. A message received IDLED seconds @@ -702,7 +780,8 @@ Slots::updateSlotAndSquelch( uint256 const& key, PublicKey const& validator, id_t id, - protocol::MessageType type) + protocol::MessageType type, + typename Slot::ignored_squelch_callback callback) { if (!addPeerMessage(key, id)) return; @@ -713,14 +792,18 @@ Slots::updateSlotAndSquelch( JLOG(journal_.trace()) << "updateSlotAndSquelch: new slot " << Slice(validator); auto it = slots_ - .emplace(std::make_pair( - validator, - Slot(handler_, logs_.journal("Slot")))) + .emplace( + std::make_pair( + validator, + Slot( + handler_, + logs_.journal("Slot"), + maxSelectedPeers_))) .first; - it->second.update(validator, id, type); + it->second.update(validator, id, type, callback); } else - it->second.update(validator, id, type); + it->second.update(validator, id, type, callback); } template diff --git a/src/xrpld/overlay/detail/ConnectAttempt.cpp b/src/xrpld/overlay/detail/ConnectAttempt.cpp index 30763b1357..84fbd36d32 100644 --- a/src/xrpld/overlay/detail/ConnectAttempt.cpp +++ b/src/xrpld/overlay/detail/ConnectAttempt.cpp @@ -209,7 +209,7 @@ ConnectAttempt::onHandshake(error_code ec) app_.config().COMPRESSION, app_.config().LEDGER_REPLAY, app_.config().TX_REDUCE_RELAY_ENABLE, - app_.config().VP_REDUCE_RELAY_ENABLE); + app_.config().VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE); buildHandshake( req_, diff --git a/src/xrpld/overlay/detail/Handshake.cpp b/src/xrpld/overlay/detail/Handshake.cpp index 657d28072f..e3617a1d98 100644 --- a/src/xrpld/overlay/detail/Handshake.cpp +++ b/src/xrpld/overlay/detail/Handshake.cpp @@ -414,7 +414,7 @@ makeResponse( app.config().COMPRESSION, app.config().LEDGER_REPLAY, app.config().TX_REDUCE_RELAY_ENABLE, - app.config().VP_REDUCE_RELAY_ENABLE)); + app.config().VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE)); buildHandshake(resp, sharedValue, networkID, public_ip, remote_ip, app); diff --git a/src/xrpld/overlay/detail/Handshake.h b/src/xrpld/overlay/detail/Handshake.h index 37f138b88b..1cd733ef56 100644 --- a/src/xrpld/overlay/detail/Handshake.h +++ b/src/xrpld/overlay/detail/Handshake.h @@ -139,7 +139,7 @@ makeResponse( // compression feature static constexpr char FEATURE_COMPR[] = "compr"; -// validation/proposal reduce-relay feature +// validation/proposal reduce-relay base squelch feature static constexpr char FEATURE_VPRR[] = "vprr"; // transaction reduce-relay feature static constexpr char FEATURE_TXRR[] = "txrr"; @@ -221,7 +221,7 @@ peerFeatureEnabled( @param txReduceRelayEnabled if true then transaction reduce-relay feature is enabled @param vpReduceRelayEnabled if true then validation/proposal reduce-relay - feature is enabled + base squelch feature is enabled @return X-Protocol-Ctl header value */ std::string @@ -241,8 +241,7 @@ makeFeaturesRequestHeader( @param txReduceRelayEnabled if true then transaction reduce-relay feature is enabled @param vpReduceRelayEnabled if true then validation/proposal reduce-relay - feature is enabled - @param vpReduceRelayEnabled if true then reduce-relay feature is enabled + base squelch feature is enabled @return X-Protocol-Ctl header value */ std::string diff --git a/src/xrpld/overlay/detail/OverlayImpl.cpp b/src/xrpld/overlay/detail/OverlayImpl.cpp index e1ccc2ee84..015a31eedb 100644 --- a/src/xrpld/overlay/detail/OverlayImpl.cpp +++ b/src/xrpld/overlay/detail/OverlayImpl.cpp @@ -133,16 +133,17 @@ OverlayImpl::OverlayImpl( , journal_(app_.journal("Overlay")) , serverHandler_(serverHandler) , m_resourceManager(resourceManager) - , m_peerFinder(PeerFinder::make_Manager( - io_service, - stopwatch(), - app_.journal("PeerFinder"), - config, - collector)) + , m_peerFinder( + PeerFinder::make_Manager( + io_service, + stopwatch(), + app_.journal("PeerFinder"), + config, + collector)) , m_resolver(resolver) , next_id_(1) , timer_count_(0) - , slots_(app.logs(), *this) + , slots_(app.logs(), *this, app.config()) , m_stats( std::bind(&OverlayImpl::collect_metrics, this), collector, @@ -1390,8 +1391,7 @@ makeSquelchMessage( void OverlayImpl::unsquelch(PublicKey const& validator, Peer::id_t id) const { - if (auto peer = findPeerByShortID(id); - peer && app_.config().VP_REDUCE_RELAY_SQUELCH) + if (auto peer = findPeerByShortID(id); peer) { // optimize - multiple message with different // validator might be sent to the same peer @@ -1405,8 +1405,7 @@ OverlayImpl::squelch( Peer::id_t id, uint32_t squelchDuration) const { - if (auto peer = findPeerByShortID(id); - peer && app_.config().VP_REDUCE_RELAY_SQUELCH) + if (auto peer = findPeerByShortID(id); peer) { peer->send(makeSquelchMessage(validator, true, squelchDuration)); } @@ -1419,6 +1418,9 @@ OverlayImpl::updateSlotAndSquelch( std::set&& peers, protocol::MessageType type) { + if (!slots_.baseSquelchReady()) + return; + if (!strand_.running_in_this_thread()) return post( strand_, @@ -1427,7 +1429,9 @@ OverlayImpl::updateSlotAndSquelch( }); for (auto id : peers) - slots_.updateSlotAndSquelch(key, validator, id, type); + slots_.updateSlotAndSquelch(key, validator, id, type, [&]() { + reportInboundTraffic(TrafficCount::squelch_ignored, 0); + }); } void @@ -1437,12 +1441,17 @@ OverlayImpl::updateSlotAndSquelch( Peer::id_t peer, protocol::MessageType type) { + if (!slots_.baseSquelchReady()) + return; + if (!strand_.running_in_this_thread()) return post(strand_, [this, key, validator, peer, type]() { updateSlotAndSquelch(key, validator, peer, type); }); - slots_.updateSlotAndSquelch(key, validator, peer, type); + slots_.updateSlotAndSquelch(key, validator, peer, type, [&]() { + reportInboundTraffic(TrafficCount::squelch_ignored, 0); + }); } void diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index bca2cfd8c7..1238833d0d 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include #include @@ -112,20 +113,21 @@ PeerImp::PeerImp( headers_, FEATURE_TXRR, app_.config().TX_REDUCE_RELAY_ENABLE)) - , vpReduceRelayEnabled_(app_.config().VP_REDUCE_RELAY_ENABLE) , ledgerReplayEnabled_(peerFeatureEnabled( headers_, FEATURE_LEDGER_REPLAY, app_.config().LEDGER_REPLAY)) , ledgerReplayMsgHandler_(app, app.getLedgerReplayer()) { - JLOG(journal_.info()) << "compression enabled " - << (compressionEnabled_ == Compressed::On) - << " vp reduce-relay enabled " - << vpReduceRelayEnabled_ - << " tx reduce-relay enabled " - << txReduceRelayEnabled_ << " on " << remote_address_ - << " " << id_; + JLOG(journal_.info()) + << "compression enabled " << (compressionEnabled_ == Compressed::On) + << " vp reduce-relay base squelch enabled " + << peerFeatureEnabled( + headers_, + FEATURE_VPRR, + app_.config().VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE) + << " tx reduce-relay enabled " << txReduceRelayEnabled_ << " on " + << remote_address_ << " " << id_; } PeerImp::~PeerImp() @@ -1282,6 +1284,18 @@ PeerImp::handleTransaction( auto stx = std::make_shared(sit); uint256 txID = stx->getTransactionID(); + // Charge strongly for attempting to relay a txn with tfInnerBatchTxn + // LCOV_EXCL_START + if (stx->isFlag(tfInnerBatchTxn) && + getCurrentTransactionRules()->enabled(featureBatch)) + { + JLOG(p_journal_.warn()) << "Ignoring Network relayed Tx containing " + "tfInnerBatchTxn (handleTransaction)."; + fee_.update(Resource::feeModerateBurdenPeer, "inner batch txn"); + return; + } + // LCOV_EXCL_STOP + int flags; constexpr std::chrono::seconds tx_interval = 10s; @@ -1720,8 +1734,7 @@ PeerImp::onMessage(std::shared_ptr const& m) { // Count unique messages (Slots has it's own 'HashRouter'), which a peer // receives within IDLED seconds since the message has been relayed. - if (reduceRelayReady() && relayed && - (stopwatch().now() - *relayed) < reduce_relay::IDLED) + if (relayed && (stopwatch().now() - *relayed) < reduce_relay::IDLED) overlay_.updateSlotAndSquelch( suppression, publicKey, id_, protocol::mtPROPOSE_LEDGER); @@ -2368,10 +2381,8 @@ PeerImp::onMessage(std::shared_ptr const& m) { // Count unique messages (Slots has it's own 'HashRouter'), which a // peer receives within IDLED seconds since the message has been - // relayed. Wait WAIT_ON_BOOTUP time to let the server establish - // connections to peers. - if (reduceRelayReady() && relayed && - (stopwatch().now() - *relayed) < reduce_relay::IDLED) + // relayed. + if (relayed && (stopwatch().now() - *relayed) < reduce_relay::IDLED) overlay_.updateSlotAndSquelch( key, val->getSignerPublic(), id_, protocol::mtVALIDATION); @@ -2838,6 +2849,18 @@ PeerImp::checkTransaction( // VFALCO TODO Rewrite to not use exceptions try { + // charge strongly for relaying batch txns + // LCOV_EXCL_START + if (stx->isFlag(tfInnerBatchTxn) && + getCurrentTransactionRules()->enabled(featureBatch)) + { + JLOG(p_journal_.warn()) << "Ignoring Network relayed Tx containing " + "tfInnerBatchTxn (checkSignature)."; + charge(Resource::feeModerateBurdenPeer, "inner batch txn"); + return; + } + // LCOV_EXCL_STOP + // Expired? if (stx->isFieldPresent(sfLastLedgerSequence) && (stx->getFieldU32(sfLastLedgerSequence) < @@ -2980,7 +3003,7 @@ PeerImp::checkPropose( // as part of the squelch logic. auto haveMessage = app_.overlay().relay( *packet, peerPos.suppressionID(), peerPos.publicKey()); - if (reduceRelayReady() && !haveMessage.empty()) + if (!haveMessage.empty()) overlay_.updateSlotAndSquelch( peerPos.suppressionID(), peerPos.publicKey(), @@ -3015,7 +3038,7 @@ PeerImp::checkValidation( // as part of the squelch logic. auto haveMessage = overlay_.relay(*packet, key, val->getSignerPublic()); - if (reduceRelayReady() && !haveMessage.empty()) + if (!haveMessage.empty()) { overlay_.updateSlotAndSquelch( key, @@ -3417,7 +3440,7 @@ PeerImp::processLedgerRequest(std::shared_ptr const& m) if (!m->has_ledgerhash()) info += ", no hash specified"; - JLOG(p_journal_.error()) + JLOG(p_journal_.warn()) << "processLedgerRequest: getNodeFat with nodeId " << *shaMapNodeId << " and ledger info type " << info << " throws exception: " << e.what(); @@ -3481,16 +3504,6 @@ PeerImp::isHighLatency() const return latency_ >= peerHighLatency; } -bool -PeerImp::reduceRelayReady() -{ - if (!reduceRelayReady_) - reduceRelayReady_ = - reduce_relay::epoch(UptimeClock::now()) > - reduce_relay::WAIT_ON_BOOTUP; - return vpReduceRelayEnabled_ && reduceRelayReady_; -} - void PeerImp::Metrics::add_message(std::uint64_t bytes) { diff --git a/src/xrpld/overlay/detail/PeerImp.h b/src/xrpld/overlay/detail/PeerImp.h index 8fbafa1ee9..d5f8e4d179 100644 --- a/src/xrpld/overlay/detail/PeerImp.h +++ b/src/xrpld/overlay/detail/PeerImp.h @@ -98,7 +98,7 @@ private: // Node public key of peer. PublicKey const publicKey_; std::string name_; - boost::shared_mutex mutable nameMutex_; + std::shared_mutex mutable nameMutex_; // The indices of the smallest and largest ledgers this peer has available // @@ -116,7 +116,6 @@ private: clock_type::time_point const creationTime_; reduce_relay::Squelch squelch_; - inline static std::atomic_bool reduceRelayReady_{false}; // Notes on thread locking: // @@ -190,9 +189,7 @@ private: hash_set txQueue_; // true if tx reduce-relay feature is enabled on the peer. bool txReduceRelayEnabled_ = false; - // true if validation/proposal reduce-relay feature is enabled - // on the peer. - bool vpReduceRelayEnabled_ = false; + bool ledgerReplayEnabled_ = false; LedgerReplayMsgHandler ledgerReplayMsgHandler_; @@ -217,7 +214,7 @@ private: total_bytes() const; private: - boost::shared_mutex mutable mutex_; + std::shared_mutex mutable mutex_; boost::circular_buffer rollingAvg_{30, 0ull}; clock_type::time_point intervalStart_{clock_type::now()}; std::uint64_t totalBytes_{0}; @@ -521,11 +518,6 @@ private: handleHaveTransactions( std::shared_ptr const& m); - // Check if reduce-relay feature is enabled and - // reduce_relay::WAIT_ON_BOOTUP time passed since the start - bool - reduceRelayReady(); - public: //-------------------------------------------------------------------------- // @@ -705,7 +697,6 @@ PeerImp::PeerImp( headers_, FEATURE_TXRR, app_.config().TX_REDUCE_RELAY_ENABLE)) - , vpReduceRelayEnabled_(app_.config().VP_REDUCE_RELAY_ENABLE) , ledgerReplayEnabled_(peerFeatureEnabled( headers_, FEATURE_LEDGER_REPLAY, @@ -714,13 +705,15 @@ PeerImp::PeerImp( { read_buffer_.commit(boost::asio::buffer_copy( read_buffer_.prepare(boost::asio::buffer_size(buffers)), buffers)); - JLOG(journal_.info()) << "compression enabled " - << (compressionEnabled_ == Compressed::On) - << " vp reduce-relay enabled " - << vpReduceRelayEnabled_ - << " tx reduce-relay enabled " - << txReduceRelayEnabled_ << " on " << remote_address_ - << " " << id_; + JLOG(journal_.info()) + << "compression enabled " << (compressionEnabled_ == Compressed::On) + << " vp reduce-relay base squelch enabled " + << peerFeatureEnabled( + headers_, + FEATURE_VPRR, + app_.config().VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE) + << " tx reduce-relay enabled " << txReduceRelayEnabled_ << " on " + << remote_address_ << " " << id_; } template diff --git a/src/xrpld/overlay/detail/TrafficCount.h b/src/xrpld/overlay/detail/TrafficCount.h index e93163683b..8dc02def5f 100644 --- a/src/xrpld/overlay/detail/TrafficCount.h +++ b/src/xrpld/overlay/detail/TrafficCount.h @@ -109,6 +109,8 @@ public: squelch, squelch_suppressed, // egress traffic amount suppressed by squelching + squelch_ignored, // the traffic amount that came from peers ignoring + // squelch messages // TMHaveSet message: get_set, // transaction sets we try to get @@ -262,6 +264,7 @@ public: {validatorlist, "validator_lists"}, {squelch, "squelch"}, {squelch_suppressed, "squelch_suppressed"}, + {squelch_ignored, "squelch_ignored"}, {get_set, "set_get"}, {share_set, "set_share"}, {ld_tsc_get, "ledger_data_Transaction_Set_candidate_get"}, @@ -326,6 +329,7 @@ protected: {validatorlist, {validatorlist}}, {squelch, {squelch}}, {squelch_suppressed, {squelch_suppressed}}, + {squelch_ignored, {squelch_ignored}}, {get_set, {get_set}}, {share_set, {share_set}}, {ld_tsc_get, {ld_tsc_get}}, diff --git a/src/xrpld/rpc/BookChanges.h b/src/xrpld/rpc/BookChanges.h index c87fa0ccf4..9d94e80b82 100644 --- a/src/xrpld/rpc/BookChanges.h +++ b/src/xrpld/rpc/BookChanges.h @@ -49,13 +49,13 @@ computeBookChanges(std::shared_ptr const& lpAccepted) std::map< std::string, std::tuple< - STAmount, // side A volume - STAmount, // side B volume - STAmount, // high rate - STAmount, // low rate - STAmount, // open rate - STAmount // close rate - >> + STAmount, // side A volume + STAmount, // side B volume + STAmount, // high rate + STAmount, // low rate + STAmount, // open rate + STAmount, // close rate + std::optional>> // optional: domain id tally; for (auto& tx : lpAccepted->txs) @@ -148,6 +148,8 @@ computeBookChanges(std::shared_ptr const& lpAccepted) else ss << p << "|" << g; + std::optional domain = finalFields[~sfDomainID]; + std::string key{ss.str()}; if (tally.find(key) == tally.end()) @@ -157,8 +159,8 @@ computeBookChanges(std::shared_ptr const& lpAccepted) rate, // high rate, // low rate, // open - rate // close - }; + rate, // close + domain}; else { // increment volume @@ -173,7 +175,8 @@ computeBookChanges(std::shared_ptr const& lpAccepted) if (std::get<3>(entry) > rate) // low std::get<3>(entry) = rate; - std::get<5>(entry) = rate; // close + std::get<5>(entry) = rate; // close + std::get<6>(entry) = domain; // domain } } } @@ -211,6 +214,10 @@ computeBookChanges(std::shared_ptr const& lpAccepted) inner[jss::low] = to_string(std::get<3>(entry.second).iou()); inner[jss::open] = to_string(std::get<4>(entry.second).iou()); inner[jss::close] = to_string(std::get<5>(entry.second).iou()); + + std::optional const domain = std::get<6>(entry.second); + if (domain) + inner[jss::domain] = to_string(*domain); } return jvObj; diff --git a/src/xrpld/rpc/detail/TransactionSign.cpp b/src/xrpld/rpc/detail/TransactionSign.cpp index 9387aba505..175fd84c9b 100644 --- a/src/xrpld/rpc/detail/TransactionSign.cpp +++ b/src/xrpld/rpc/detail/TransactionSign.cpp @@ -40,6 +40,7 @@ #include #include +#include namespace ripple { namespace RPC { @@ -222,6 +223,22 @@ checkPayment( rpcINVALID_PARAMS, "Cannot specify both 'tx_json.Paths' and 'build_path'"); + std::optional domain; + if (tx_json.isMember(sfDomainID.jsonName)) + { + uint256 num; + if (!tx_json[sfDomainID.jsonName].isString() || + !num.parseHex(tx_json[sfDomainID.jsonName].asString())) + { + return RPC::make_error( + rpcDOMAIN_MALFORMED, "Unable to parse 'DomainID'."); + } + else + { + domain = num; + } + } + if (!tx_json.isMember(jss::Paths) && params.isMember(jss::build_path)) { STAmount sendMax; @@ -260,6 +277,7 @@ checkPayment( sendMax.issue().account, amount, std::nullopt, + domain, app); if (pf.findPaths(app.config().PATH_SEARCH_OLD)) { @@ -464,9 +482,6 @@ transactionPreProcessImpl( hasTicketSeq ? 0 : app.getTxQ().nextQueuableSeq(sle).value(); } - if (!tx_json.isMember(jss::Flags)) - tx_json[jss::Flags] = tfFullyCanonicalSig; - if (!tx_json.isMember(jss::NetworkID)) { auto const networkId = app.config().NETWORK_ID; @@ -749,6 +764,7 @@ transactionFormatResultImpl(Transaction::pointer tpTrans, unsigned apiVersion) [[nodiscard]] static XRPAmount getTxFee(Application const& app, Config const& config, Json::Value tx) { + auto const& ledger = app.openLedger().current(); // autofilling only needed in this function so that the `STParsedJSONObject` // parsing works properly it should not be modifying the actual `tx` object if (!tx.isMember(jss::Fee)) @@ -776,6 +792,9 @@ getTxFee(Application const& app, Config const& config, Json::Value tx) if (!tx[jss::Signers].isArray()) return config.FEES.reference_fee; + if (tx[jss::Signers].size() > STTx::maxMultiSigners(&ledger->rules())) + return config.FEES.reference_fee; + // check multi-signed signers for (auto& signer : tx[jss::Signers]) { @@ -804,6 +823,10 @@ getTxFee(Application const& app, Config const& config, Json::Value tx) try { STTx const& stTx = STTx(std::move(parsed.object.value())); + std::string reason; + if (!passesLocalChecks(stTx, reason)) + return config.FEES.reference_fee; + return calculateBaseFee(*app.openLedger().current(), stTx); } catch (std::exception& e) diff --git a/src/xrpld/rpc/handlers/AccountInfo.cpp b/src/xrpld/rpc/handlers/AccountInfo.cpp index 6416309e2e..3432021690 100644 --- a/src/xrpld/rpc/handlers/AccountInfo.cpp +++ b/src/xrpld/rpc/handlers/AccountInfo.cpp @@ -108,6 +108,10 @@ doAccountInfo(RPC::JsonContext& context) allowTrustLineClawbackFlag{ "allowTrustLineClawback", lsfAllowTrustLineClawback}; + static constexpr std::pair + allowTrustLineLockingFlag{ + "allowTrustLineLocking", lsfAllowTrustLineLocking}; + auto const sleAccepted = ledger->read(keylet::account(accountID)); if (sleAccepted) { @@ -140,6 +144,10 @@ doAccountInfo(RPC::JsonContext& context) acctFlags[allowTrustLineClawbackFlag.first.data()] = sleAccepted->isFlag(allowTrustLineClawbackFlag.second); + if (ledger->rules().enabled(featureTokenEscrow)) + acctFlags[allowTrustLineLockingFlag.first.data()] = + sleAccepted->isFlag(allowTrustLineLockingFlag.second); + result[jss::account_flags] = std::move(acctFlags); // The document[https://xrpl.org/account_info.html#account_info] states diff --git a/src/xrpld/rpc/handlers/AccountTx.cpp b/src/xrpld/rpc/handlers/AccountTx.cpp index 26c8065edf..d5df40303b 100644 --- a/src/xrpld/rpc/handlers/AccountTx.cpp +++ b/src/xrpld/rpc/handlers/AccountTx.cpp @@ -348,7 +348,7 @@ populateJsonResponse( txnMeta->getJson(JsonOptions::include_date); insertDeliveredAmount( jvObj[jss::meta], context, txn, *txnMeta); - insertNFTSyntheticInJson(jvObj, sttx, *txnMeta); + RPC::insertNFTSyntheticInJson(jvObj, sttx, *txnMeta); RPC::insertMPTokenIssuanceID( jvObj[jss::meta], sttx, *txnMeta); } diff --git a/src/xrpld/rpc/handlers/BookOffers.cpp b/src/xrpld/rpc/handlers/BookOffers.cpp index bede01b927..df4712209c 100644 --- a/src/xrpld/rpc/handlers/BookOffers.cpp +++ b/src/xrpld/rpc/handlers/BookOffers.cpp @@ -172,6 +172,22 @@ doBookOffers(RPC::JsonContext& context) return RPC::invalid_field_error(jss::taker); } + std::optional domain; + if (context.params.isMember(jss::domain)) + { + uint256 num; + if (!context.params[jss::domain].isString() || + !num.parseHex(context.params[jss::domain].asString())) + { + return RPC::make_error( + rpcDOMAIN_MALFORMED, "Unable to parse domain."); + } + else + { + domain = num; + } + } + if (pay_currency == get_currency && pay_issuer == get_issuer) { JLOG(context.j.info()) << "taker_gets same as taker_pays."; @@ -190,7 +206,7 @@ doBookOffers(RPC::JsonContext& context) context.netOps.getBookPage( lpLedger, - {{pay_currency, pay_issuer}, {get_currency, get_issuer}}, + {{pay_currency, pay_issuer}, {get_currency, get_issuer}, domain}, takerID ? *takerID : beast::zero, bProof, limit, diff --git a/src/xrpld/rpc/handlers/GatewayBalances.cpp b/src/xrpld/rpc/handlers/GatewayBalances.cpp index e8b95bd75c..ca9e370c81 100644 --- a/src/xrpld/rpc/handlers/GatewayBalances.cpp +++ b/src/xrpld/rpc/handlers/GatewayBalances.cpp @@ -142,11 +142,41 @@ doGatewayBalances(RPC::JsonContext& context) std::map> hotBalances; std::map> assets; std::map> frozenBalances; + std::map locked; // Traverse the cold wallet's trust lines { forEachItem( *ledger, accountID, [&](std::shared_ptr const& sle) { + if (sle->getType() == ltESCROW) + { + auto const& escrow = sle->getFieldAmount(sfAmount); + auto& bal = locked[escrow.getCurrency()]; + if (bal == beast::zero) + { + // This is needed to set the currency code correctly + bal = escrow; + } + else + { + try + { + bal += escrow; + } + catch (std::runtime_error const&) + { + // Presumably the exception was caused by overflow. + // On overflow return the largest valid STAmount. + // Very large sums of STAmount are approximations + // anyway. + bal = STAmount( + bal.issue(), + STAmount::cMaxValue, + STAmount::cMaxOffset); + } + } + } + auto rs = PathFindTrustLine::makeItem(accountID, sle); if (!rs) @@ -246,6 +276,17 @@ doGatewayBalances(RPC::JsonContext& context) populateResult(frozenBalances, jss::frozen_balances); populateResult(assets, jss::assets); + // Add total escrow to the result + if (!locked.empty()) + { + Json::Value j; + for (auto const& [k, v] : locked) + { + j[to_string(k)] = v.getText(); + } + result[jss::locked] = std::move(j); + } + return result; } diff --git a/src/xrpld/rpc/handlers/LedgerHandler.cpp b/src/xrpld/rpc/handlers/LedgerHandler.cpp index 4015bb9fcc..8987f2d07e 100644 --- a/src/xrpld/rpc/handlers/LedgerHandler.cpp +++ b/src/xrpld/rpc/handlers/LedgerHandler.cpp @@ -54,10 +54,6 @@ LedgerHandler::check() bool const binary = params[jss::binary].asBool(); bool const owner_funds = params[jss::owner_funds].asBool(); bool const queue = params[jss::queue].asBool(); - auto type = chooseLedgerEntryType(params); - if (type.first) - return type.first; - type_ = type.second; options_ = (full ? LedgerFill::full : 0) | (expand ? LedgerFill::expand : 0) | diff --git a/src/xrpld/rpc/handlers/LedgerHandler.h b/src/xrpld/rpc/handlers/LedgerHandler.h index 0e47164ad3..a573589cbc 100644 --- a/src/xrpld/rpc/handlers/LedgerHandler.h +++ b/src/xrpld/rpc/handlers/LedgerHandler.h @@ -76,7 +76,6 @@ private: std::vector queueTxs_; Json::Value result_; int options_ = 0; - LedgerEntryType type_; }; //////////////////////////////////////////////////////////////////////////////// @@ -91,7 +90,7 @@ LedgerHandler::writeResult(Object& value) if (ledger_) { Json::copyFrom(value, result_); - addJson(value, {*ledger_, &context_, options_, queueTxs_, type_}); + addJson(value, {*ledger_, &context_, options_, queueTxs_}); } else { @@ -105,6 +104,21 @@ LedgerHandler::writeResult(Object& value) addJson(open, {*master.getCurrentLedger(), &context_, 0}); } } + + Json::Value warnings{Json::arrayValue}; + if (context_.params.isMember(jss::type)) + { + Json::Value& w = warnings.append(Json::objectValue); + w[jss::id] = warnRPC_FIELDS_DEPRECATED; + w[jss::message] = + "Some fields from your request are deprecated. Please check the " + "documentation at " + "https://xrpl.org/docs/references/http-websocket-apis/ " + "and update your request. Field `type` is deprecated."; + } + + if (warnings.size()) + value[jss::warnings] = std::move(warnings); } } // namespace RPC diff --git a/src/xrpld/rpc/handlers/Simulate.cpp b/src/xrpld/rpc/handlers/Simulate.cpp index 5f69c203ff..3c175883c5 100644 --- a/src/xrpld/rpc/handlers/Simulate.cpp +++ b/src/xrpld/rpc/handlers/Simulate.cpp @@ -342,6 +342,11 @@ doSimulate(RPC::JsonContext& context) return jvResult; } + if (stTx->getTxnType() == ttBATCH) + { + return RPC::make_error(rpcNOT_IMPL); + } + std::string reason; auto transaction = std::make_shared(stTx, reason, context.app); // Actually run the transaction through the transaction processor diff --git a/src/xrpld/rpc/handlers/Stop.cpp b/src/xrpld/rpc/handlers/Stop.cpp index 03e73fb6b7..95da27dc62 100644 --- a/src/xrpld/rpc/handlers/Stop.cpp +++ b/src/xrpld/rpc/handlers/Stop.cpp @@ -31,7 +31,7 @@ struct JsonContext; Json::Value doStop(RPC::JsonContext& context) { - context.app.signalStop(); + context.app.signalStop("RPC"); return RPC::makeObjectValue(systemName() + " server stopping"); } diff --git a/src/xrpld/rpc/handlers/Subscribe.cpp b/src/xrpld/rpc/handlers/Subscribe.cpp index deac6e18ad..e71d973b7b 100644 --- a/src/xrpld/rpc/handlers/Subscribe.cpp +++ b/src/xrpld/rpc/handlers/Subscribe.cpp @@ -305,6 +305,20 @@ doSubscribe(RPC::JsonContext& context) return rpcError(rpcBAD_ISSUER); } + if (j.isMember(jss::domain)) + { + uint256 domain; + if (!j[jss::domain].isString() || + !domain.parseHex(j[jss::domain].asString())) + { + return rpcError(rpcDOMAIN_MALFORMED); + } + else + { + book.domain = domain; + } + } + if (!isConsistent(book)) { JLOG(context.j.warn()) << "Bad market: " << book; diff --git a/src/xrpld/rpc/handlers/Tx.cpp b/src/xrpld/rpc/handlers/Tx.cpp index 3db71d9002..d43a699ab3 100644 --- a/src/xrpld/rpc/handlers/Tx.cpp +++ b/src/xrpld/rpc/handlers/Tx.cpp @@ -270,7 +270,7 @@ populateJsonResponse( response[jss::meta] = meta->getJson(JsonOptions::none); insertDeliveredAmount( response[jss::meta], context, result.txn, *meta); - insertNFTSyntheticInJson(response, sttx, *meta); + RPC::insertNFTSyntheticInJson(response, sttx, *meta); RPC::insertMPTokenIssuanceID(response[jss::meta], sttx, *meta); } } diff --git a/src/xrpld/rpc/handlers/Unsubscribe.cpp b/src/xrpld/rpc/handlers/Unsubscribe.cpp index c890de593a..f512840c86 100644 --- a/src/xrpld/rpc/handlers/Unsubscribe.cpp +++ b/src/xrpld/rpc/handlers/Unsubscribe.cpp @@ -230,6 +230,20 @@ doUnsubscribe(RPC::JsonContext& context) return rpcError(rpcBAD_MARKET); } + if (jv.isMember(jss::domain)) + { + uint256 domain; + if (!jv[jss::domain].isString() || + !domain.parseHex(jv[jss::domain].asString())) + { + return rpcError(rpcDOMAIN_MALFORMED); + } + else + { + book.domain = domain; + } + } + context.netOps.unsubBook(ispSub->getSeq(), book); // both_sides is deprecated. diff --git a/src/xrpld/shamap/SHAMap.h b/src/xrpld/shamap/SHAMap.h index 33c42c2d23..738cf96ecc 100644 --- a/src/xrpld/shamap/SHAMap.h +++ b/src/xrpld/shamap/SHAMap.h @@ -36,6 +36,7 @@ #include #include +#include #include #include diff --git a/tests/conan/CMakeLists.txt b/tests/conan/CMakeLists.txt index 83aa24880d..f1b37e7a69 100644 --- a/tests/conan/CMakeLists.txt +++ b/tests/conan/CMakeLists.txt @@ -9,7 +9,7 @@ project( LANGUAGES CXX ) -find_package(xrpl REQUIRED) +find_package(xrpl CONFIG REQUIRED) add_executable(example) target_sources(example PRIVATE src/example.cpp) diff --git a/tests/conan/conanfile.py b/tests/conan/conanfile.py index be3750bf9e..1ea1b333fc 100644 --- a/tests/conan/conanfile.py +++ b/tests/conan/conanfile.py @@ -1,59 +1,42 @@ -from conan import ConanFile, conan_version +from pathlib import Path + +from conan import ConanFile +from conan.tools.build import can_run from conan.tools.cmake import CMake, cmake_layout class Example(ConanFile): - def set_name(self): - if self.name is None: - self.name = 'example' + name = 'example' + license = 'ISC' + author = 'John Freeman , Michael Legleux