From fc33a8e0ce58f1fa5a5d9454874b3e220e0129d4 Mon Sep 17 00:00:00 2001 From: Bart Thomee <11445373+bthomee@users.noreply.github.com> Date: Thu, 23 Oct 2025 10:59:35 -0400 Subject: [PATCH 01/28] Use gnu-sed on MacOS --- .github/scripts/rename/definitions.sh | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/.github/scripts/rename/definitions.sh b/.github/scripts/rename/definitions.sh index d9f04b91e6..53fcffe860 100755 --- a/.github/scripts/rename/definitions.sh +++ b/.github/scripts/rename/definitions.sh @@ -1,5 +1,18 @@ #!/bin/bash +# Exit the script as soon as an error occurs. +set -e + +# On MacOS, ensure that GNU sed is installed and available as `gsed`. +SED_COMMAND=sed +if [[ "$OSTYPE" == "darwin"* ]]; then + if ! command -v gsed &> /dev/null; then + echo "Error: gsed is not installed. Please install it using 'brew install gnu-sed'." + exit 1 + fi + SED_COMMAND=gsed +fi + # This script renames definitions, such as include guards, in this project. # Specifically, it renames "RIPPLED_XXX" and "RIPPLE_XXX" to "XRPL_XXX" by # scanning all cmake, header, and source files in the specified directory and @@ -20,12 +33,10 @@ fi find "${DIRECTORY}" -type f \( -name "*.h" -o -name "*.hpp" -o -name "*.ipp" -o -name "*.cpp" \) | while read -r FILE; do echo "Processing file: ${FILE}" - sed -i'.bak' -E 's@#(define|endif|if|ifdef|ifndef)(.*)(RIPPLED_|RIPPLE_)([A-Z0-9_]+)@#\1\2XRPL_\4@g' "${FILE}" - rm "${FILE}.bak" + ${SED_COMMAND} -i -E 's@#(define|endif|if|ifdef|ifndef)(.*)(RIPPLED_|RIPPLE_)([A-Z0-9_]+)@#\1\2XRPL_\4@g' "${FILE}" done find "${DIRECTORY}" -type f \( -name "*.cmake" -o -name "*.txt" \) | while read -r FILE; do echo "Processing file: ${FILE}" - sed -i'.bak' -E 's@(RIPPLED_|RIPPLE_)([A-Z0-9_]+)@XRPL_\2@g' "${FILE}" - rm "${FILE}.bak" + ${SED_COMMAND} -i -E 's@(RIPPLED_|RIPPLE_)([A-Z0-9_]+)@XRPL_\2@g' "${FILE}" done echo "Renaming complete." From e192ffe964e6e3e3d6eae6734e9de7bd7a039cdb Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Fri, 24 Oct 2025 16:16:15 +0100 Subject: [PATCH 02/28] fix: Clean up build profile options (#5934) The `-Wno-missing-template-arg-list-after-template-kw` flag is only needed for the grpc library. Use `+=` for the default build flags to make it easier to extend in the future. Co-authored-by: Bart Thomee <11445373+bthomee@users.noreply.github.com> --- conan/profiles/default | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/conan/profiles/default b/conan/profiles/default index 03f19ca118..def2fffd6b 100644 --- a/conan/profiles/default +++ b/conan/profiles/default @@ -21,11 +21,11 @@ compiler.libcxx={{detect_api.detect_libcxx(compiler, version, compiler_exe)}} [conf] {% if compiler == "clang" and compiler_version >= 19 %} -tools.build:cxxflags=['-Wno-missing-template-arg-list-after-template-kw'] +grpc/1.50.1:tools.build:cxxflags+=['-Wno-missing-template-arg-list-after-template-kw'] {% endif %} {% if compiler == "apple-clang" and compiler_version >= 17 %} -tools.build:cxxflags=['-Wno-missing-template-arg-list-after-template-kw'] +grpc/1.50.1:tools.build:cxxflags+=['-Wno-missing-template-arg-list-after-template-kw'] {% endif %} {% if compiler == "gcc" and compiler_version < 13 %} -tools.build:cxxflags=['-Wno-restrict'] +tools.build:cxxflags+=['-Wno-restrict'] {% endif %} From 1845b1c656e5a9030e9f4caf02f2a99f9ce747fd Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Mon, 27 Oct 2025 14:43:45 +0000 Subject: [PATCH 03/28] chore: Better pre-commit failure message (#5940) --- .github/workflows/pre-commit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 66ee2f3334..d0a657dd7e 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -9,7 +9,7 @@ on: jobs: # Call the workflow in the XRPLF/actions repo that runs the pre-commit hooks. run-hooks: - uses: XRPLF/actions/.github/workflows/pre-commit.yml@a8d7472b450eb53a1e5228f64552e5974457a21a + uses: XRPLF/actions/.github/workflows/pre-commit.yml@34790936fae4c6c751f62ec8c06696f9c1a5753a with: runs_on: ubuntu-latest container: '{ "image": "ghcr.io/xrplf/ci/tools-rippled-pre-commit:sha-a8c7be1" }' From 66f16469f9ed7923a1bd2003bc9e73ba10829a17 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Mon, 27 Oct 2025 17:27:56 +0000 Subject: [PATCH 04/28] fix: Upload all test binaries (#5932) --- .github/workflows/reusable-build.yml | 18 +++++++++++++++++- .github/workflows/reusable-test.yml | 21 +++++++++++++++++++-- cmake/xrpl_add_test.cmake | 2 +- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/.github/workflows/reusable-build.yml b/.github/workflows/reusable-build.yml index 24650858f9..09f7f25271 100644 --- a/.github/workflows/reusable-build.yml +++ b/.github/workflows/reusable-build.yml @@ -101,11 +101,27 @@ jobs: --parallel $(nproc) \ --target "${CMAKE_TARGET}" + - name: Put built binaries in one location + shell: bash + working-directory: ${{ inputs.build_dir }} + env: + BUILD_TYPE_DIR: ${{ runner.os == 'Windows' && inputs.build_type || '' }} + CMAKE_TARGET: ${{ inputs.cmake_target }} + run: | + mkdir -p ./binaries/doctest/ + + cp ./${BUILD_TYPE_DIR}/rippled* ./binaries/ + if [ "${CMAKE_TARGET}" != 'coverage' ]; then + cp ./src/tests/libxrpl/${BUILD_TYPE_DIR}/xrpl.test.* ./binaries/doctest/ + fi + - name: Upload rippled artifact uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + env: + BUILD_DIR: ${{ inputs.build_dir }} with: name: rippled-${{ inputs.config_name }} - path: ${{ inputs.build_dir }}/${{ runner.os == 'Windows' && inputs.build_type || '' }}/rippled${{ runner.os == 'Windows' && '.exe' || '' }} + path: ${{ env.BUILD_DIR }}/binaries/ retention-days: 3 if-no-files-found: error diff --git a/.github/workflows/reusable-test.yml b/.github/workflows/reusable-test.yml index eb6a0271a4..cfd0fe6849 100644 --- a/.github/workflows/reusable-test.yml +++ b/.github/workflows/reusable-test.yml @@ -33,6 +33,10 @@ jobs: container: ${{ inputs.image != '' && inputs.image || null }} timeout-minutes: 30 steps: + - name: Cleanup workspace + if: ${{ runner.os == 'macOS' }} + uses: XRPLF/actions/.github/actions/cleanup-workspace@3f044c7478548e3c32ff68980eeb36ece02b364e + - name: Download rippled artifact uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: @@ -62,9 +66,22 @@ jobs: run: | ./rippled --version | grep libvoidstar - - name: Test the binary + - name: Run the embedded tests if: ${{ inputs.run_tests }} shell: bash run: | ./rippled --unittest --unittest-jobs $(nproc) - ctest -j $(nproc) --output-on-failure + + - name: Run the separate tests + if: ${{ inputs.run_tests }} + shell: bash + run: | + for test_file in ./doctest/*; do + echo "Executing $test_file" + chmod +x "$test_file" + if [[ "${{ runner.os }}" == "Windows" && "$test_file" == "./doctest/xrpl.test.net.exe" ]]; then + echo "Skipping $test_file on Windows" + else + "$test_file" + fi + done diff --git a/cmake/xrpl_add_test.cmake b/cmake/xrpl_add_test.cmake index d61f4ece3d..a1dd3847bd 100644 --- a/cmake/xrpl_add_test.cmake +++ b/cmake/xrpl_add_test.cmake @@ -7,7 +7,7 @@ function(xrpl_add_test name) "${CMAKE_CURRENT_SOURCE_DIR}/${name}/*.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/${name}.cpp" ) - add_executable(${target} EXCLUDE_FROM_ALL ${ARGN} ${sources}) + add_executable(${target} ${ARGN} ${sources}) isolate_headers( ${target} From f4f76181732d1fbe1d31d15fb6ab47d79e067fe3 Mon Sep 17 00:00:00 2001 From: Shawn Xie <35279399+shawnxie999@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:04:14 -0400 Subject: [PATCH 05/28] Change `fixMPTDeliveredAmount` to `Supported::yes` (#5833) Co-authored-by: Bart Thomee <11445373+bthomee@users.noreply.github.com> --- include/xrpl/protocol/detail/features.macro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 32dc1a1e3c..6052814357 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -37,7 +37,7 @@ XRPL_FEATURE(DynamicMPT, Supported::no, VoteBehavior::DefaultNo XRPL_FIX (TokenEscrowV1, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (DelegateV1_1, Supported::no, VoteBehavior::DefaultNo) XRPL_FIX (PriceOracleOrder, Supported::yes, VoteBehavior::DefaultNo) -XRPL_FIX (MPTDeliveredAmount, Supported::no, VoteBehavior::DefaultNo) +XRPL_FIX (MPTDeliveredAmount, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (AMMClawbackRounding, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(TokenEscrow, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (EnforceNFTokenTrustlineV2, Supported::yes, VoteBehavior::DefaultNo) From 76f774e22d67490638121f9ce53da87495ed72f9 Mon Sep 17 00:00:00 2001 From: Jingchen Date: Tue, 28 Oct 2025 14:19:39 +0000 Subject: [PATCH 06/28] refactor: Migrate json unit tests to use doctest (#5533) Co-authored-by: Bart Thomee <11445373+bthomee@users.noreply.github.com> --- .../scripts/levelization/results/ordering.txt | 1 + src/test/json/Writer_test.cpp | 217 --- src/test/json/json_value_test.cpp | 1398 ----------------- src/tests/libxrpl/CMakeLists.txt | 2 + .../libxrpl/json/Output.cpp} | 71 +- src/tests/libxrpl/json/Value.cpp | 1294 +++++++++++++++ src/tests/libxrpl/json/Writer.cpp | 192 +++ src/tests/libxrpl/json/main.cpp | 2 + 8 files changed, 1523 insertions(+), 1654 deletions(-) delete mode 100644 src/test/json/Writer_test.cpp delete mode 100644 src/test/json/json_value_test.cpp rename src/{test/json/Output_test.cpp => tests/libxrpl/json/Output.cpp} (51%) create mode 100644 src/tests/libxrpl/json/Value.cpp create mode 100644 src/tests/libxrpl/json/Writer.cpp create mode 100644 src/tests/libxrpl/json/main.cpp diff --git a/.github/scripts/levelization/results/ordering.txt b/.github/scripts/levelization/results/ordering.txt index 55df4c2672..e17bd14bbc 100644 --- a/.github/scripts/levelization/results/ordering.txt +++ b/.github/scripts/levelization/results/ordering.txt @@ -138,6 +138,7 @@ test.toplevel > test.csf test.toplevel > xrpl.json test.unit_test > xrpl.basics tests.libxrpl > xrpl.basics +tests.libxrpl > xrpl.json tests.libxrpl > xrpl.net xrpl.json > xrpl.basics xrpl.ledger > xrpl.basics diff --git a/src/test/json/Writer_test.cpp b/src/test/json/Writer_test.cpp deleted file mode 100644 index 3739af07e1..0000000000 --- a/src/test/json/Writer_test.cpp +++ /dev/null @@ -1,217 +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 -#include - -namespace Json { - -class JsonWriter_test : public ripple::test::TestOutputSuite -{ -public: - void - testTrivial() - { - setup("trivial"); - BEAST_EXPECT(output_.empty()); - expectResult(""); - } - - void - testNearTrivial() - { - setup("near trivial"); - BEAST_EXPECT(output_.empty()); - writer_->output(0); - expectResult("0"); - } - - void - testPrimitives() - { - setup("true"); - writer_->output(true); - expectResult("true"); - - setup("false"); - writer_->output(false); - expectResult("false"); - - setup("23"); - writer_->output(23); - expectResult("23"); - - setup("23.0"); - writer_->output(23.0); - expectResult("23.0"); - - setup("23.5"); - writer_->output(23.5); - expectResult("23.5"); - - setup("a string"); - writer_->output("a string"); - expectResult("\"a string\""); - - setup("nullptr"); - writer_->output(nullptr); - expectResult("null"); - } - - void - testEmpty() - { - setup("empty array"); - writer_->startRoot(Writer::array); - writer_->finish(); - expectResult("[]"); - - setup("empty object"); - writer_->startRoot(Writer::object); - writer_->finish(); - expectResult("{}"); - } - - void - testEscaping() - { - setup("backslash"); - writer_->output("\\"); - expectResult("\"\\\\\""); - - setup("quote"); - writer_->output("\""); - expectResult("\"\\\"\""); - - setup("backslash and quote"); - writer_->output("\\\""); - expectResult("\"\\\\\\\"\""); - - setup("escape embedded"); - writer_->output("this contains a \\ in the middle of it."); - expectResult("\"this contains a \\\\ in the middle of it.\""); - - setup("remaining escapes"); - writer_->output("\b\f\n\r\t"); - expectResult("\"\\b\\f\\n\\r\\t\""); - } - - void - testArray() - { - setup("empty array"); - writer_->startRoot(Writer::array); - writer_->append(12); - writer_->finish(); - expectResult("[12]"); - } - - void - testLongArray() - { - setup("long array"); - writer_->startRoot(Writer::array); - writer_->append(12); - writer_->append(true); - writer_->append("hello"); - writer_->finish(); - expectResult("[12,true,\"hello\"]"); - } - - void - testEmbeddedArraySimple() - { - setup("embedded array simple"); - writer_->startRoot(Writer::array); - writer_->startAppend(Writer::array); - writer_->finish(); - writer_->finish(); - expectResult("[[]]"); - } - - void - testObject() - { - setup("object"); - writer_->startRoot(Writer::object); - writer_->set("hello", "world"); - writer_->finish(); - - expectResult("{\"hello\":\"world\"}"); - } - - void - testComplexObject() - { - setup("complex object"); - writer_->startRoot(Writer::object); - - writer_->set("hello", "world"); - writer_->startSet(Writer::array, "array"); - - writer_->append(true); - writer_->append(12); - writer_->startAppend(Writer::array); - writer_->startAppend(Writer::object); - writer_->set("goodbye", "cruel world."); - writer_->startSet(Writer::array, "subarray"); - writer_->append(23.5); - writer_->finishAll(); - - expectResult( - "{\"hello\":\"world\",\"array\":[true,12," - "[{\"goodbye\":\"cruel world.\"," - "\"subarray\":[23.5]}]]}"); - } - - void - testJson() - { - setup("object"); - Json::Value value(Json::objectValue); - value["foo"] = 23; - writer_->startRoot(Writer::object); - writer_->set("hello", value); - writer_->finish(); - - expectResult("{\"hello\":{\"foo\":23}}"); - } - - void - run() override - { - testTrivial(); - testNearTrivial(); - testPrimitives(); - testEmpty(); - testEscaping(); - testArray(); - testLongArray(); - testEmbeddedArraySimple(); - testObject(); - testComplexObject(); - testJson(); - } -}; - -BEAST_DEFINE_TESTSUITE(JsonWriter, json, ripple); - -} // namespace Json diff --git a/src/test/json/json_value_test.cpp b/src/test/json/json_value_test.cpp deleted file mode 100644 index 0858786f04..0000000000 --- a/src/test/json/json_value_test.cpp +++ /dev/null @@ -1,1398 +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 -#include -#include -#include - -#include -#include - -namespace ripple { - -struct json_value_test : beast::unit_test::suite -{ - void - test_StaticString() - { - static constexpr char sample[]{"Contents of a Json::StaticString"}; - - static constexpr Json::StaticString test1(sample); - char const* addrTest1{test1}; - - BEAST_EXPECT(addrTest1 == &sample[0]); - BEAST_EXPECT(test1.c_str() == &sample[0]); - - static constexpr Json::StaticString test2{ - "Contents of a Json::StaticString"}; - static constexpr Json::StaticString test3{"Another StaticString"}; - - BEAST_EXPECT(test1 == test2); - BEAST_EXPECT(test1 != test3); - - std::string str{sample}; - BEAST_EXPECT(str == test2); - BEAST_EXPECT(str != test3); - BEAST_EXPECT(test2 == str); - BEAST_EXPECT(test3 != str); - } - - void - test_types() - { - // Exercise ValueType constructor - static constexpr Json::StaticString staticStr{"staticStr"}; - - auto testCopy = [this](Json::ValueType typ) { - Json::Value val{typ}; - Json::Value cpy{val}; - BEAST_EXPECT(val.type() == typ); - BEAST_EXPECT(cpy.type() == typ); - return val; - }; - { - Json::Value const nullV{testCopy(Json::nullValue)}; - BEAST_EXPECT(nullV.isNull()); - BEAST_EXPECT(!nullV.isBool()); - BEAST_EXPECT(!nullV.isInt()); - BEAST_EXPECT(!nullV.isUInt()); - BEAST_EXPECT(!nullV.isIntegral()); - BEAST_EXPECT(!nullV.isDouble()); - BEAST_EXPECT(!nullV.isNumeric()); - BEAST_EXPECT(!nullV.isString()); - BEAST_EXPECT(!nullV.isArray()); - BEAST_EXPECT(nullV.isArrayOrNull()); - BEAST_EXPECT(!nullV.isObject()); - BEAST_EXPECT(nullV.isObjectOrNull()); - } - { - Json::Value const intV{testCopy(Json::intValue)}; - BEAST_EXPECT(!intV.isNull()); - BEAST_EXPECT(!intV.isBool()); - BEAST_EXPECT(intV.isInt()); - BEAST_EXPECT(!intV.isUInt()); - BEAST_EXPECT(intV.isIntegral()); - BEAST_EXPECT(!intV.isDouble()); - BEAST_EXPECT(intV.isNumeric()); - BEAST_EXPECT(!intV.isString()); - BEAST_EXPECT(!intV.isArray()); - BEAST_EXPECT(!intV.isArrayOrNull()); - BEAST_EXPECT(!intV.isObject()); - BEAST_EXPECT(!intV.isObjectOrNull()); - } - { - Json::Value const uintV{testCopy(Json::uintValue)}; - BEAST_EXPECT(!uintV.isNull()); - BEAST_EXPECT(!uintV.isBool()); - BEAST_EXPECT(!uintV.isInt()); - BEAST_EXPECT(uintV.isUInt()); - BEAST_EXPECT(uintV.isIntegral()); - BEAST_EXPECT(!uintV.isDouble()); - BEAST_EXPECT(uintV.isNumeric()); - BEAST_EXPECT(!uintV.isString()); - BEAST_EXPECT(!uintV.isArray()); - BEAST_EXPECT(!uintV.isArrayOrNull()); - BEAST_EXPECT(!uintV.isObject()); - BEAST_EXPECT(!uintV.isObjectOrNull()); - } - { - Json::Value const realV{testCopy(Json::realValue)}; - BEAST_EXPECT(!realV.isNull()); - BEAST_EXPECT(!realV.isBool()); - BEAST_EXPECT(!realV.isInt()); - BEAST_EXPECT(!realV.isUInt()); - BEAST_EXPECT(!realV.isIntegral()); - BEAST_EXPECT(realV.isDouble()); - BEAST_EXPECT(realV.isNumeric()); - BEAST_EXPECT(!realV.isString()); - BEAST_EXPECT(!realV.isArray()); - BEAST_EXPECT(!realV.isArrayOrNull()); - BEAST_EXPECT(!realV.isObject()); - BEAST_EXPECT(!realV.isObjectOrNull()); - } - { - Json::Value const stringV{testCopy(Json::stringValue)}; - BEAST_EXPECT(!stringV.isNull()); - BEAST_EXPECT(!stringV.isBool()); - BEAST_EXPECT(!stringV.isInt()); - BEAST_EXPECT(!stringV.isUInt()); - BEAST_EXPECT(!stringV.isIntegral()); - BEAST_EXPECT(!stringV.isDouble()); - BEAST_EXPECT(!stringV.isNumeric()); - BEAST_EXPECT(stringV.isString()); - BEAST_EXPECT(!stringV.isArray()); - BEAST_EXPECT(!stringV.isArrayOrNull()); - BEAST_EXPECT(!stringV.isObject()); - BEAST_EXPECT(!stringV.isObjectOrNull()); - } - { - Json::Value const staticStrV{staticStr}; - { - Json::Value cpy{staticStrV}; - BEAST_EXPECT(staticStrV.type() == Json::stringValue); - BEAST_EXPECT(cpy.type() == Json::stringValue); - } - BEAST_EXPECT(!staticStrV.isNull()); - BEAST_EXPECT(!staticStrV.isBool()); - BEAST_EXPECT(!staticStrV.isInt()); - BEAST_EXPECT(!staticStrV.isUInt()); - BEAST_EXPECT(!staticStrV.isIntegral()); - BEAST_EXPECT(!staticStrV.isDouble()); - BEAST_EXPECT(!staticStrV.isNumeric()); - BEAST_EXPECT(staticStrV.isString()); - BEAST_EXPECT(!staticStrV.isArray()); - BEAST_EXPECT(!staticStrV.isArrayOrNull()); - BEAST_EXPECT(!staticStrV.isObject()); - BEAST_EXPECT(!staticStrV.isObjectOrNull()); - } - { - Json::Value const boolV{testCopy(Json::booleanValue)}; - BEAST_EXPECT(!boolV.isNull()); - BEAST_EXPECT(boolV.isBool()); - BEAST_EXPECT(!boolV.isInt()); - BEAST_EXPECT(!boolV.isUInt()); - BEAST_EXPECT(boolV.isIntegral()); - BEAST_EXPECT(!boolV.isDouble()); - BEAST_EXPECT(boolV.isNumeric()); - BEAST_EXPECT(!boolV.isString()); - BEAST_EXPECT(!boolV.isArray()); - BEAST_EXPECT(!boolV.isArrayOrNull()); - BEAST_EXPECT(!boolV.isObject()); - BEAST_EXPECT(!boolV.isObjectOrNull()); - } - { - Json::Value const arrayV{testCopy(Json::arrayValue)}; - BEAST_EXPECT(!arrayV.isNull()); - BEAST_EXPECT(!arrayV.isBool()); - BEAST_EXPECT(!arrayV.isInt()); - BEAST_EXPECT(!arrayV.isUInt()); - BEAST_EXPECT(!arrayV.isIntegral()); - BEAST_EXPECT(!arrayV.isDouble()); - BEAST_EXPECT(!arrayV.isNumeric()); - BEAST_EXPECT(!arrayV.isString()); - BEAST_EXPECT(arrayV.isArray()); - BEAST_EXPECT(arrayV.isArrayOrNull()); - BEAST_EXPECT(!arrayV.isObject()); - BEAST_EXPECT(!arrayV.isObjectOrNull()); - } - { - Json::Value const objectV{testCopy(Json::objectValue)}; - BEAST_EXPECT(!objectV.isNull()); - BEAST_EXPECT(!objectV.isBool()); - BEAST_EXPECT(!objectV.isInt()); - BEAST_EXPECT(!objectV.isUInt()); - BEAST_EXPECT(!objectV.isIntegral()); - BEAST_EXPECT(!objectV.isDouble()); - BEAST_EXPECT(!objectV.isNumeric()); - BEAST_EXPECT(!objectV.isString()); - BEAST_EXPECT(!objectV.isArray()); - BEAST_EXPECT(!objectV.isArrayOrNull()); - BEAST_EXPECT(objectV.isObject()); - BEAST_EXPECT(objectV.isObjectOrNull()); - } - } - - void - test_compare() - { - auto doCompare = [this]( - Json::Value const& lhs, - Json::Value const& rhs, - bool lhsEqRhs, - bool lhsLtRhs, - int line) { - auto fmt = [this](bool cond, char const* text, int line) { - if (cond) - this->pass(); - else - this->fail(text, __FILE__, line); - }; - fmt((lhs == rhs) == lhsEqRhs, "Value ==", line); - fmt((lhs != rhs) != lhsEqRhs, "Value !=", line); - fmt((lhs < rhs) == (!(lhsEqRhs | !lhsLtRhs)), "Value <", line); - fmt((lhs <= rhs) == (lhsEqRhs | lhsLtRhs), "Value <=", line); - fmt((lhs >= rhs) == (lhsEqRhs | !lhsLtRhs), "Value >=", line); - fmt((lhs > rhs) == (!(lhsEqRhs | lhsLtRhs)), "Value >", line); - }; - - Json::Value const null0; - Json::Value const intNeg1{-1}; - Json::Value const int0{Json::intValue}; - Json::Value const intPos1{1}; - Json::Value const uint0{Json::uintValue}; - Json::Value const uint1{1u}; - Json::Value const realNeg1{-1.0}; - Json::Value const real0{Json::realValue}; - Json::Value const realPos1{1.0}; - Json::Value const str0{Json::stringValue}; - Json::Value const str1{"1"}; - Json::Value const boolF{false}; - Json::Value const boolT{true}; - Json::Value const array0{Json::arrayValue}; - Json::Value const array1{[]() { - Json::Value array1; - array1[0u] = 1; - return array1; - }()}; - Json::Value const obj0{Json::objectValue}; - Json::Value const obj1{[]() { - Json::Value obj1; - obj1["one"] = 1; - return obj1; - }()}; - // lhs == rhs lhs < rhs - doCompare(null0, Json::Value{}, true, false, __LINE__); - doCompare(null0, intNeg1, false, true, __LINE__); - doCompare(null0, int0, false, true, __LINE__); - doCompare(null0, intPos1, false, true, __LINE__); - doCompare(null0, uint0, false, true, __LINE__); - doCompare(null0, uint1, false, true, __LINE__); - doCompare(null0, realNeg1, false, true, __LINE__); - doCompare(null0, real0, false, true, __LINE__); - doCompare(null0, realPos1, false, true, __LINE__); - doCompare(null0, str0, false, true, __LINE__); - doCompare(null0, str1, false, true, __LINE__); - doCompare(null0, boolF, false, true, __LINE__); - doCompare(null0, boolT, false, true, __LINE__); - doCompare(null0, array0, false, true, __LINE__); - doCompare(null0, array1, false, true, __LINE__); - doCompare(null0, obj0, false, true, __LINE__); - doCompare(null0, obj1, false, true, __LINE__); - // lhs == rhs lhs < rhs - doCompare(intNeg1, null0, false, false, __LINE__); - doCompare(intNeg1, intNeg1, true, false, __LINE__); - doCompare(intNeg1, int0, false, true, __LINE__); - doCompare(intNeg1, intPos1, false, true, __LINE__); - doCompare(intNeg1, uint0, false, true, __LINE__); - doCompare(intNeg1, uint1, false, true, __LINE__); - doCompare(intNeg1, realNeg1, false, true, __LINE__); - doCompare(intNeg1, real0, false, true, __LINE__); - doCompare(intNeg1, realPos1, false, true, __LINE__); - doCompare(intNeg1, str0, false, true, __LINE__); - doCompare(intNeg1, str1, false, true, __LINE__); - doCompare(intNeg1, boolF, false, true, __LINE__); - doCompare(intNeg1, boolT, false, true, __LINE__); - doCompare(intNeg1, array0, false, true, __LINE__); - doCompare(intNeg1, array1, false, true, __LINE__); - doCompare(intNeg1, obj0, false, true, __LINE__); - doCompare(intNeg1, obj1, false, true, __LINE__); - // lhs == rhs lhs < rhs - doCompare(int0, null0, false, false, __LINE__); - doCompare(int0, intNeg1, false, false, __LINE__); - doCompare(int0, int0, true, false, __LINE__); - doCompare(int0, intPos1, false, true, __LINE__); - doCompare(int0, uint0, true, false, __LINE__); - doCompare(int0, uint1, false, true, __LINE__); - doCompare(int0, realNeg1, false, true, __LINE__); - doCompare(int0, real0, false, true, __LINE__); - doCompare(int0, realPos1, false, true, __LINE__); - doCompare(int0, str0, false, true, __LINE__); - doCompare(int0, str1, false, true, __LINE__); - doCompare(int0, boolF, false, true, __LINE__); - doCompare(int0, boolT, false, true, __LINE__); - doCompare(int0, array0, false, true, __LINE__); - doCompare(int0, array1, false, true, __LINE__); - doCompare(int0, obj0, false, true, __LINE__); - doCompare(int0, obj1, false, true, __LINE__); - // lhs == rhs lhs < rhs - doCompare(intPos1, null0, false, false, __LINE__); - doCompare(intPos1, intNeg1, false, false, __LINE__); - doCompare(intPos1, int0, false, false, __LINE__); - doCompare(intPos1, intPos1, true, false, __LINE__); - doCompare(intPos1, uint0, false, false, __LINE__); - doCompare(intPos1, uint1, true, false, __LINE__); - doCompare(intPos1, realNeg1, false, true, __LINE__); - doCompare(intPos1, real0, false, true, __LINE__); - doCompare(intPos1, realPos1, false, true, __LINE__); - doCompare(intPos1, str0, false, true, __LINE__); - doCompare(intPos1, str1, false, true, __LINE__); - doCompare(intPos1, boolF, false, true, __LINE__); - doCompare(intPos1, boolT, false, true, __LINE__); - doCompare(intPos1, array0, false, true, __LINE__); - doCompare(intPos1, array1, false, true, __LINE__); - doCompare(intPos1, obj0, false, true, __LINE__); - doCompare(intPos1, obj1, false, true, __LINE__); - // lhs == rhs lhs < rhs - doCompare(uint0, null0, false, false, __LINE__); - doCompare(uint0, intNeg1, false, false, __LINE__); - doCompare(uint0, int0, true, false, __LINE__); - doCompare(uint0, intPos1, false, true, __LINE__); - doCompare(uint0, uint0, true, false, __LINE__); - doCompare(uint0, uint1, false, true, __LINE__); - doCompare(uint0, realNeg1, false, true, __LINE__); - doCompare(uint0, real0, false, true, __LINE__); - doCompare(uint0, realPos1, false, true, __LINE__); - doCompare(uint0, str0, false, true, __LINE__); - doCompare(uint0, str1, false, true, __LINE__); - doCompare(uint0, boolF, false, true, __LINE__); - doCompare(uint0, boolT, false, true, __LINE__); - doCompare(uint0, array0, false, true, __LINE__); - doCompare(uint0, array1, false, true, __LINE__); - doCompare(uint0, obj0, false, true, __LINE__); - doCompare(uint0, obj1, false, true, __LINE__); - // lhs == rhs lhs < rhs - doCompare(uint1, null0, false, false, __LINE__); - doCompare(uint1, intNeg1, false, false, __LINE__); - doCompare(uint1, int0, false, false, __LINE__); - doCompare(uint1, intPos1, true, false, __LINE__); - doCompare(uint1, uint0, false, false, __LINE__); - doCompare(uint1, uint1, true, false, __LINE__); - doCompare(uint1, realNeg1, false, true, __LINE__); - doCompare(uint1, real0, false, true, __LINE__); - doCompare(uint1, realPos1, false, true, __LINE__); - doCompare(uint1, str0, false, true, __LINE__); - doCompare(uint1, str1, false, true, __LINE__); - doCompare(uint1, boolF, false, true, __LINE__); - doCompare(uint1, boolT, false, true, __LINE__); - doCompare(uint1, array0, false, true, __LINE__); - doCompare(uint1, array1, false, true, __LINE__); - doCompare(uint1, obj0, false, true, __LINE__); - doCompare(uint1, obj1, false, true, __LINE__); - // lhs == rhs lhs < rhs - doCompare(realNeg1, null0, false, false, __LINE__); - doCompare(realNeg1, intNeg1, false, false, __LINE__); - doCompare(realNeg1, int0, false, false, __LINE__); - doCompare(realNeg1, intPos1, false, false, __LINE__); - doCompare(realNeg1, uint0, false, false, __LINE__); - doCompare(realNeg1, uint1, false, false, __LINE__); - doCompare(realNeg1, realNeg1, true, false, __LINE__); - doCompare(realNeg1, real0, false, true, __LINE__); - doCompare(realNeg1, realPos1, false, true, __LINE__); - doCompare(realNeg1, str0, false, true, __LINE__); - doCompare(realNeg1, str1, false, true, __LINE__); - doCompare(realNeg1, boolF, false, true, __LINE__); - doCompare(realNeg1, boolT, false, true, __LINE__); - doCompare(realNeg1, array0, false, true, __LINE__); - doCompare(realNeg1, array1, false, true, __LINE__); - doCompare(realNeg1, obj0, false, true, __LINE__); - doCompare(realNeg1, obj1, false, true, __LINE__); - // lhs == rhs lhs < rhs - doCompare(real0, null0, false, false, __LINE__); - doCompare(real0, intNeg1, false, false, __LINE__); - doCompare(real0, int0, false, false, __LINE__); - doCompare(real0, intPos1, false, false, __LINE__); - doCompare(real0, uint0, false, false, __LINE__); - doCompare(real0, uint1, false, false, __LINE__); - doCompare(real0, realNeg1, false, false, __LINE__); - doCompare(real0, real0, true, false, __LINE__); - doCompare(real0, realPos1, false, true, __LINE__); - doCompare(real0, str0, false, true, __LINE__); - doCompare(real0, str1, false, true, __LINE__); - doCompare(real0, boolF, false, true, __LINE__); - doCompare(real0, boolT, false, true, __LINE__); - doCompare(real0, array0, false, true, __LINE__); - doCompare(real0, array1, false, true, __LINE__); - doCompare(real0, obj0, false, true, __LINE__); - doCompare(real0, obj1, false, true, __LINE__); - // lhs == rhs lhs < rhs - doCompare(realPos1, null0, false, false, __LINE__); - doCompare(realPos1, intNeg1, false, false, __LINE__); - doCompare(realPos1, int0, false, false, __LINE__); - doCompare(realPos1, intPos1, false, false, __LINE__); - doCompare(realPos1, uint0, false, false, __LINE__); - doCompare(realPos1, uint1, false, false, __LINE__); - doCompare(realPos1, realNeg1, false, false, __LINE__); - doCompare(realPos1, real0, false, false, __LINE__); - doCompare(realPos1, realPos1, true, false, __LINE__); - doCompare(realPos1, str0, false, true, __LINE__); - doCompare(realPos1, str1, false, true, __LINE__); - doCompare(realPos1, boolF, false, true, __LINE__); - doCompare(realPos1, boolT, false, true, __LINE__); - doCompare(realPos1, array0, false, true, __LINE__); - doCompare(realPos1, array1, false, true, __LINE__); - doCompare(realPos1, obj0, false, true, __LINE__); - doCompare(realPos1, obj1, false, true, __LINE__); - // lhs == rhs lhs < rhs - doCompare(str0, null0, false, false, __LINE__); - doCompare(str0, intNeg1, false, false, __LINE__); - doCompare(str0, int0, false, false, __LINE__); - doCompare(str0, intPos1, false, false, __LINE__); - doCompare(str0, uint0, false, false, __LINE__); - doCompare(str0, uint1, false, false, __LINE__); - doCompare(str0, realNeg1, false, false, __LINE__); - doCompare(str0, real0, false, false, __LINE__); - doCompare(str0, realPos1, false, false, __LINE__); - doCompare(str0, str0, true, false, __LINE__); - doCompare(str0, str1, false, true, __LINE__); - doCompare(str0, boolF, false, true, __LINE__); - doCompare(str0, boolT, false, true, __LINE__); - doCompare(str0, array0, false, true, __LINE__); - doCompare(str0, array1, false, true, __LINE__); - doCompare(str0, obj0, false, true, __LINE__); - doCompare(str0, obj1, false, true, __LINE__); - // lhs == rhs lhs < rhs - doCompare(str1, null0, false, false, __LINE__); - doCompare(str1, intNeg1, false, false, __LINE__); - doCompare(str1, int0, false, false, __LINE__); - doCompare(str1, intPos1, false, false, __LINE__); - doCompare(str1, uint0, false, false, __LINE__); - doCompare(str1, uint1, false, false, __LINE__); - doCompare(str1, realNeg1, false, false, __LINE__); - doCompare(str1, real0, false, false, __LINE__); - doCompare(str1, realPos1, false, false, __LINE__); - doCompare(str1, str0, false, false, __LINE__); - doCompare(str1, str1, true, false, __LINE__); - doCompare(str1, boolF, false, true, __LINE__); - doCompare(str1, boolT, false, true, __LINE__); - doCompare(str1, array0, false, true, __LINE__); - doCompare(str1, array1, false, true, __LINE__); - doCompare(str1, obj0, false, true, __LINE__); - doCompare(str1, obj1, false, true, __LINE__); - // lhs == rhs lhs < rhs - doCompare(boolF, null0, false, false, __LINE__); - doCompare(boolF, intNeg1, false, false, __LINE__); - doCompare(boolF, int0, false, false, __LINE__); - doCompare(boolF, intPos1, false, false, __LINE__); - doCompare(boolF, uint0, false, false, __LINE__); - doCompare(boolF, uint1, false, false, __LINE__); - doCompare(boolF, realNeg1, false, false, __LINE__); - doCompare(boolF, real0, false, false, __LINE__); - doCompare(boolF, realPos1, false, false, __LINE__); - doCompare(boolF, str0, false, false, __LINE__); - doCompare(boolF, str1, false, false, __LINE__); - doCompare(boolF, boolF, true, false, __LINE__); - doCompare(boolF, boolT, false, true, __LINE__); - doCompare(boolF, array0, false, true, __LINE__); - doCompare(boolF, array1, false, true, __LINE__); - doCompare(boolF, obj0, false, true, __LINE__); - doCompare(boolF, obj1, false, true, __LINE__); - // lhs == rhs lhs < rhs - doCompare(boolT, null0, false, false, __LINE__); - doCompare(boolT, intNeg1, false, false, __LINE__); - doCompare(boolT, int0, false, false, __LINE__); - doCompare(boolT, intPos1, false, false, __LINE__); - doCompare(boolT, uint0, false, false, __LINE__); - doCompare(boolT, uint1, false, false, __LINE__); - doCompare(boolT, realNeg1, false, false, __LINE__); - doCompare(boolT, real0, false, false, __LINE__); - doCompare(boolT, realPos1, false, false, __LINE__); - doCompare(boolT, str0, false, false, __LINE__); - doCompare(boolT, str1, false, false, __LINE__); - doCompare(boolT, boolF, false, false, __LINE__); - doCompare(boolT, boolT, true, false, __LINE__); - doCompare(boolT, array0, false, true, __LINE__); - doCompare(boolT, array1, false, true, __LINE__); - doCompare(boolT, obj0, false, true, __LINE__); - doCompare(boolT, obj1, false, true, __LINE__); - // lhs == rhs lhs < rhs - doCompare(array0, null0, false, false, __LINE__); - doCompare(array0, intNeg1, false, false, __LINE__); - doCompare(array0, int0, false, false, __LINE__); - doCompare(array0, intPos1, false, false, __LINE__); - doCompare(array0, uint0, false, false, __LINE__); - doCompare(array0, uint1, false, false, __LINE__); - doCompare(array0, realNeg1, false, false, __LINE__); - doCompare(array0, real0, false, false, __LINE__); - doCompare(array0, realPos1, false, false, __LINE__); - doCompare(array0, str0, false, false, __LINE__); - doCompare(array0, str1, false, false, __LINE__); - doCompare(array0, boolF, false, false, __LINE__); - doCompare(array0, boolT, false, false, __LINE__); - doCompare(array0, array0, true, false, __LINE__); - doCompare(array0, array1, false, true, __LINE__); - doCompare(array0, obj0, false, true, __LINE__); - doCompare(array0, obj1, false, true, __LINE__); - // lhs == rhs lhs < rhs - doCompare(array1, null0, false, false, __LINE__); - doCompare(array1, intNeg1, false, false, __LINE__); - doCompare(array1, int0, false, false, __LINE__); - doCompare(array1, intPos1, false, false, __LINE__); - doCompare(array1, uint0, false, false, __LINE__); - doCompare(array1, uint1, false, false, __LINE__); - doCompare(array1, realNeg1, false, false, __LINE__); - doCompare(array1, real0, false, false, __LINE__); - doCompare(array1, realPos1, false, false, __LINE__); - doCompare(array1, str0, false, false, __LINE__); - doCompare(array1, str1, false, false, __LINE__); - doCompare(array1, boolF, false, false, __LINE__); - doCompare(array1, boolT, false, false, __LINE__); - doCompare(array1, array0, false, false, __LINE__); - doCompare(array1, array1, true, false, __LINE__); - doCompare(array1, obj0, false, true, __LINE__); - doCompare(array1, obj1, false, true, __LINE__); - // lhs == rhs lhs < rhs - doCompare(obj0, null0, false, false, __LINE__); - doCompare(obj0, intNeg1, false, false, __LINE__); - doCompare(obj0, int0, false, false, __LINE__); - doCompare(obj0, intPos1, false, false, __LINE__); - doCompare(obj0, uint0, false, false, __LINE__); - doCompare(obj0, uint1, false, false, __LINE__); - doCompare(obj0, realNeg1, false, false, __LINE__); - doCompare(obj0, real0, false, false, __LINE__); - doCompare(obj0, realPos1, false, false, __LINE__); - doCompare(obj0, str0, false, false, __LINE__); - doCompare(obj0, str1, false, false, __LINE__); - doCompare(obj0, boolF, false, false, __LINE__); - doCompare(obj0, boolT, false, false, __LINE__); - doCompare(obj0, array0, false, false, __LINE__); - doCompare(obj0, array1, false, false, __LINE__); - doCompare(obj0, obj0, true, false, __LINE__); - doCompare(obj0, obj1, false, true, __LINE__); - // lhs == rhs lhs < rhs - doCompare(obj1, null0, false, false, __LINE__); - doCompare(obj1, intNeg1, false, false, __LINE__); - doCompare(obj1, int0, false, false, __LINE__); - doCompare(obj1, intPos1, false, false, __LINE__); - doCompare(obj1, uint0, false, false, __LINE__); - doCompare(obj1, uint1, false, false, __LINE__); - doCompare(obj1, realNeg1, false, false, __LINE__); - doCompare(obj1, real0, false, false, __LINE__); - doCompare(obj1, realPos1, false, false, __LINE__); - doCompare(obj1, str0, false, false, __LINE__); - doCompare(obj1, str1, false, false, __LINE__); - doCompare(obj1, boolF, false, false, __LINE__); - doCompare(obj1, boolT, false, false, __LINE__); - doCompare(obj1, array0, false, false, __LINE__); - doCompare(obj1, array1, false, false, __LINE__); - doCompare(obj1, obj0, false, false, __LINE__); - doCompare(obj1, obj1, true, false, __LINE__); - } - - void - test_bool() - { - BEAST_EXPECT(!Json::Value()); - - BEAST_EXPECT(!Json::Value("")); - - BEAST_EXPECT(bool(Json::Value("empty"))); - BEAST_EXPECT(bool(Json::Value(false))); - BEAST_EXPECT(bool(Json::Value(true))); - BEAST_EXPECT(bool(Json::Value(0))); - BEAST_EXPECT(bool(Json::Value(1))); - - Json::Value array(Json::arrayValue); - BEAST_EXPECT(!array); - array.append(0); - BEAST_EXPECT(bool(array)); - - Json::Value object(Json::objectValue); - BEAST_EXPECT(!object); - object[""] = false; - BEAST_EXPECT(bool(object)); - } - - void - test_bad_json() - { - char const* s( - "{\"method\":\"ledger\",\"params\":[{\"ledger_index\":1e300}]}"); - - Json::Value j; - Json::Reader r; - - r.parse(s, j); - pass(); - } - - void - test_edge_cases() - { - std::string json; - - std::uint32_t max_uint = std::numeric_limits::max(); - std::int32_t max_int = std::numeric_limits::max(); - std::int32_t min_int = std::numeric_limits::min(); - - std::uint32_t a_uint = max_uint - 1978; - std::int32_t a_large_int = max_int - 1978; - std::int32_t a_small_int = min_int + 1978; - - json = "{\"max_uint\":" + std::to_string(max_uint); - json += ",\"max_int\":" + std::to_string(max_int); - json += ",\"min_int\":" + std::to_string(min_int); - json += ",\"a_uint\":" + std::to_string(a_uint); - json += ",\"a_large_int\":" + std::to_string(a_large_int); - json += ",\"a_small_int\":" + std::to_string(a_small_int); - json += "}"; - - Json::Value j1; - Json::Reader r1; - - BEAST_EXPECT(r1.parse(json, j1)); - BEAST_EXPECT(j1["max_uint"].asUInt() == max_uint); - BEAST_EXPECT(j1["max_int"].asInt() == max_int); - BEAST_EXPECT(j1["min_int"].asInt() == min_int); - BEAST_EXPECT(j1["a_uint"].asUInt() == a_uint); - BEAST_EXPECT(j1["a_uint"] > a_large_int); - BEAST_EXPECT(j1["a_uint"] > a_small_int); - BEAST_EXPECT(j1["a_large_int"].asInt() == a_large_int); - BEAST_EXPECT(j1["a_large_int"].asUInt() == a_large_int); - BEAST_EXPECT(j1["a_large_int"] < a_uint); - BEAST_EXPECT(j1["a_small_int"].asInt() == a_small_int); - BEAST_EXPECT(j1["a_small_int"] < a_uint); - - json = "{\"overflow\":"; - json += std::to_string(std::uint64_t(max_uint) + 1); - json += "}"; - - Json::Value j2; - Json::Reader r2; - - BEAST_EXPECT(!r2.parse(json, j2)); - - json = "{\"underflow\":"; - json += std::to_string(std::int64_t(min_int) - 1); - json += "}"; - - Json::Value j3; - Json::Reader r3; - - BEAST_EXPECT(!r3.parse(json, j3)); - - Json::Value intString{"4294967296"}; - try - { - [[maybe_unused]] std::uint32_t const uTooBig{intString.asUInt()}; - fail("4294967296", __FILE__, __LINE__); - } - catch (beast::BadLexicalCast const&) - { - pass(); - } - - intString = "4294967295"; - BEAST_EXPECT(intString.asUInt() == 4294967295u); - - intString = "0"; - BEAST_EXPECT(intString.asUInt() == 0); - - intString = "-1"; - try - { - [[maybe_unused]] std::uint32_t const uTooSmall{intString.asUInt()}; - fail("-1", __FILE__, __LINE__); - } - catch (beast::BadLexicalCast const&) - { - pass(); - } - - intString = "2147483648"; - try - { - [[maybe_unused]] std::int32_t tooPos{intString.asInt()}; - fail("2147483648", __FILE__, __LINE__); - } - catch (beast::BadLexicalCast const&) - { - pass(); - } - - intString = "2147483647"; - BEAST_EXPECT(intString.asInt() == 2147483647); - - intString = "-2147483648"; - BEAST_EXPECT(intString.asInt() == -2147483648LL); // MSVC wants the LL - - intString = "-2147483649"; - try - { - [[maybe_unused]] std::int32_t tooNeg{intString.asInt()}; - fail("-2147483649", __FILE__, __LINE__); - } - catch (beast::BadLexicalCast const&) - { - pass(); - } - } - - void - test_copy() - { - Json::Value v1{2.5}; - BEAST_EXPECT(v1.isDouble()); - BEAST_EXPECT(v1.asDouble() == 2.5); - - Json::Value v2 = v1; - BEAST_EXPECT(v1.isDouble()); - BEAST_EXPECT(v1.asDouble() == 2.5); - BEAST_EXPECT(v2.isDouble()); - BEAST_EXPECT(v2.asDouble() == 2.5); - BEAST_EXPECT(v1 == v2); - - v1 = v2; - BEAST_EXPECT(v1.isDouble()); - BEAST_EXPECT(v1.asDouble() == 2.5); - BEAST_EXPECT(v2.isDouble()); - BEAST_EXPECT(v2.asDouble() == 2.5); - BEAST_EXPECT(v1 == v2); - - pass(); - } - - void - test_move() - { - Json::Value v1{2.5}; - BEAST_EXPECT(v1.isDouble()); - BEAST_EXPECT(v1.asDouble() == 2.5); - - Json::Value v2 = std::move(v1); - BEAST_EXPECT(!v1); - BEAST_EXPECT(v2.isDouble()); - BEAST_EXPECT(v2.asDouble() == 2.5); - BEAST_EXPECT(v1 != v2); - - v1 = std::move(v2); - BEAST_EXPECT(v1.isDouble()); - BEAST_EXPECT(v1.asDouble() == 2.5); - BEAST_EXPECT(!v2); - BEAST_EXPECT(v1 != v2); - - pass(); - } - - void - test_comparisons() - { - Json::Value a, b; - auto testEquals = [&](std::string const& name) { - BEAST_EXPECT(a == b); - BEAST_EXPECT(a <= b); - BEAST_EXPECT(a >= b); - - BEAST_EXPECT(!(a != b)); - BEAST_EXPECT(!(a < b)); - BEAST_EXPECT(!(a > b)); - - BEAST_EXPECT(b == a); - BEAST_EXPECT(b <= a); - BEAST_EXPECT(b >= a); - - BEAST_EXPECT(!(b != a)); - BEAST_EXPECT(!(b < a)); - BEAST_EXPECT(!(b > a)); - }; - - auto testGreaterThan = [&](std::string const& name) { - BEAST_EXPECT(!(a == b)); - BEAST_EXPECT(!(a <= b)); - BEAST_EXPECT(a >= b); - - BEAST_EXPECT(a != b); - BEAST_EXPECT(!(a < b)); - BEAST_EXPECT(a > b); - - BEAST_EXPECT(!(b == a)); - BEAST_EXPECT(b <= a); - BEAST_EXPECT(!(b >= a)); - - BEAST_EXPECT(b != a); - BEAST_EXPECT(b < a); - BEAST_EXPECT(!(b > a)); - }; - - a["a"] = Json::UInt(0); - b["a"] = Json::Int(0); - testEquals("zero"); - - b["a"] = Json::Int(-1); - testGreaterThan("negative"); - - Json::Int big = std::numeric_limits::max(); - Json::UInt bigger = big; - bigger++; - - a["a"] = bigger; - b["a"] = big; - testGreaterThan("big"); - } - - void - test_compact() - { - Json::Value j; - Json::Reader r; - char const* s("{\"array\":[{\"12\":23},{},null,false,0.5]}"); - - auto countLines = [](std::string const& str) { - return 1 + std::count_if(str.begin(), str.end(), [](char c) { - return c == '\n'; - }); - }; - - BEAST_EXPECT(r.parse(s, j)); - { - std::stringstream ss; - ss << j; - BEAST_EXPECT(countLines(ss.str()) > 1); - } - { - std::stringstream ss; - ss << Json::Compact(std::move(j)); - BEAST_EXPECT(countLines(ss.str()) == 1); - } - } - - void - test_conversions() - { - // We have Json::Int, but not Json::Double or Json::Real. - // We have Json::Int, Json::Value::Int, and Json::ValueType::intValue. - // We have Json::ValueType::realValue but Json::Value::asDouble. - // TODO: What's the thinking here? - { - // null - Json::Value val; - BEAST_EXPECT(val.isNull()); - // BEAST_EXPECT(strcmp (val.asCString(), ?) == 0); // - // asserts - BEAST_EXPECT(val.asString() == ""); - BEAST_EXPECT(val.asInt() == 0); - BEAST_EXPECT(val.asUInt() == 0); - BEAST_EXPECT(val.asDouble() == 0.0); - BEAST_EXPECT(val.asBool() == false); - - BEAST_EXPECT(val.isConvertibleTo(Json::nullValue)); - BEAST_EXPECT(val.isConvertibleTo(Json::intValue)); - BEAST_EXPECT(val.isConvertibleTo(Json::uintValue)); - BEAST_EXPECT(val.isConvertibleTo(Json::realValue)); - BEAST_EXPECT(val.isConvertibleTo(Json::stringValue)); - BEAST_EXPECT(val.isConvertibleTo(Json::booleanValue)); - BEAST_EXPECT(val.isConvertibleTo(Json::arrayValue)); - BEAST_EXPECT(val.isConvertibleTo(Json::objectValue)); - } - { - // int - Json::Value val = -1234; - BEAST_EXPECT(val.isInt()); - // BEAST_EXPECT(strcmp (val.asCString(), ?) == 0); // - // asserts - BEAST_EXPECT(val.asString() == "-1234"); - BEAST_EXPECT(val.asInt() == -1234); - // BEAST_EXPECT(val.asUInt() == ?); // - // asserts or throws - BEAST_EXPECT(val.asDouble() == -1234.0); - BEAST_EXPECT(val.asBool() == true); - - BEAST_EXPECT(!val.isConvertibleTo(Json::nullValue)); - BEAST_EXPECT(val.isConvertibleTo(Json::intValue)); - BEAST_EXPECT(!val.isConvertibleTo(Json::uintValue)); - BEAST_EXPECT(val.isConvertibleTo(Json::realValue)); - BEAST_EXPECT(val.isConvertibleTo(Json::stringValue)); - BEAST_EXPECT(val.isConvertibleTo(Json::booleanValue)); - BEAST_EXPECT(!val.isConvertibleTo(Json::arrayValue)); - BEAST_EXPECT(!val.isConvertibleTo(Json::objectValue)); - } - { - // uint - Json::Value val = 1234U; - BEAST_EXPECT(val.isUInt()); - // BEAST_EXPECT(strcmp (val.asCString(), ?) == 0); // - // asserts - BEAST_EXPECT(val.asString() == "1234"); - BEAST_EXPECT(val.asInt() == 1234); - BEAST_EXPECT(val.asUInt() == 1234u); - BEAST_EXPECT(val.asDouble() == 1234.0); - BEAST_EXPECT(val.asBool() == true); - - BEAST_EXPECT(!val.isConvertibleTo(Json::nullValue)); - BEAST_EXPECT(val.isConvertibleTo(Json::intValue)); - BEAST_EXPECT(val.isConvertibleTo(Json::uintValue)); - BEAST_EXPECT(val.isConvertibleTo(Json::realValue)); - BEAST_EXPECT(val.isConvertibleTo(Json::stringValue)); - BEAST_EXPECT(val.isConvertibleTo(Json::booleanValue)); - BEAST_EXPECT(!val.isConvertibleTo(Json::arrayValue)); - BEAST_EXPECT(!val.isConvertibleTo(Json::objectValue)); - } - { - // real - Json::Value val = 2.0; - BEAST_EXPECT(val.isDouble()); - // BEAST_EXPECT(strcmp (val.asCString(), ?) == 0); // - // asserts - BEAST_EXPECT( - std::regex_match(val.asString(), std::regex("^2\\.0*$"))); - BEAST_EXPECT(val.asInt() == 2); - BEAST_EXPECT(val.asUInt() == 2u); - BEAST_EXPECT(val.asDouble() == 2.0); - BEAST_EXPECT(val.asBool() == true); - - BEAST_EXPECT(!val.isConvertibleTo(Json::nullValue)); - BEAST_EXPECT(val.isConvertibleTo(Json::intValue)); - BEAST_EXPECT(val.isConvertibleTo(Json::uintValue)); - BEAST_EXPECT(val.isConvertibleTo(Json::realValue)); - BEAST_EXPECT(val.isConvertibleTo(Json::stringValue)); - BEAST_EXPECT(val.isConvertibleTo(Json::booleanValue)); - BEAST_EXPECT(!val.isConvertibleTo(Json::arrayValue)); - BEAST_EXPECT(!val.isConvertibleTo(Json::objectValue)); - } - { - // numeric string - Json::Value val = "54321"; - BEAST_EXPECT(val.isString()); - BEAST_EXPECT(strcmp(val.asCString(), "54321") == 0); - BEAST_EXPECT(val.asString() == "54321"); - BEAST_EXPECT(val.asInt() == 54321); - BEAST_EXPECT(val.asUInt() == 54321u); - // BEAST_EXPECT(val.asDouble() == 54321.0); // - // asserts or throws - BEAST_EXPECT(val.asBool() == true); - - BEAST_EXPECT(!val.isConvertibleTo(Json::nullValue)); - BEAST_EXPECT(!val.isConvertibleTo(Json::intValue)); - BEAST_EXPECT(!val.isConvertibleTo(Json::uintValue)); - BEAST_EXPECT(!val.isConvertibleTo(Json::realValue)); - BEAST_EXPECT(val.isConvertibleTo(Json::stringValue)); - BEAST_EXPECT(!val.isConvertibleTo(Json::booleanValue)); - BEAST_EXPECT(!val.isConvertibleTo(Json::arrayValue)); - BEAST_EXPECT(!val.isConvertibleTo(Json::objectValue)); - } - { - // non-numeric string - Json::Value val(Json::stringValue); - BEAST_EXPECT(val.isString()); - BEAST_EXPECT(val.asCString() == nullptr); - BEAST_EXPECT(val.asString() == ""); - try - { - BEAST_EXPECT(val.asInt() == 0); - fail("expected exception", __FILE__, __LINE__); - } - catch (std::exception const&) - { - pass(); - } - try - { - BEAST_EXPECT(val.asUInt() == 0); - fail("expected exception", __FILE__, __LINE__); - } - catch (std::exception const&) - { - pass(); - } - // BEAST_EXPECT(val.asDouble() == ?); // - // asserts or throws - BEAST_EXPECT(val.asBool() == false); - - BEAST_EXPECT(val.isConvertibleTo(Json::nullValue)); - BEAST_EXPECT(!val.isConvertibleTo(Json::intValue)); - BEAST_EXPECT(!val.isConvertibleTo(Json::uintValue)); - BEAST_EXPECT(!val.isConvertibleTo(Json::realValue)); - BEAST_EXPECT(val.isConvertibleTo(Json::stringValue)); - BEAST_EXPECT(!val.isConvertibleTo(Json::booleanValue)); - BEAST_EXPECT(!val.isConvertibleTo(Json::arrayValue)); - BEAST_EXPECT(!val.isConvertibleTo(Json::objectValue)); - } - { - // bool false - Json::Value val = false; - BEAST_EXPECT(val.isBool()); - // BEAST_EXPECT(strcmp (val.asCString(), ?) == 0); // - // asserts - BEAST_EXPECT(val.asString() == "false"); - BEAST_EXPECT(val.asInt() == 0); - BEAST_EXPECT(val.asUInt() == 0); - BEAST_EXPECT(val.asDouble() == 0.0); - BEAST_EXPECT(val.asBool() == false); - - BEAST_EXPECT(val.isConvertibleTo(Json::nullValue)); - BEAST_EXPECT(val.isConvertibleTo(Json::intValue)); - BEAST_EXPECT(val.isConvertibleTo(Json::uintValue)); - BEAST_EXPECT(val.isConvertibleTo(Json::realValue)); - BEAST_EXPECT(val.isConvertibleTo(Json::stringValue)); - BEAST_EXPECT(val.isConvertibleTo(Json::booleanValue)); - BEAST_EXPECT(!val.isConvertibleTo(Json::arrayValue)); - BEAST_EXPECT(!val.isConvertibleTo(Json::objectValue)); - } - { - // bool true - Json::Value val = true; - BEAST_EXPECT(val.isBool()); - // BEAST_EXPECT(strcmp (val.asCString(), ?) == 0); // - // asserts - BEAST_EXPECT(val.asString() == "true"); - BEAST_EXPECT(val.asInt() == 1); - BEAST_EXPECT(val.asUInt() == 1); - BEAST_EXPECT(val.asDouble() == 1.0); - BEAST_EXPECT(val.asBool() == true); - - BEAST_EXPECT(!val.isConvertibleTo(Json::nullValue)); - BEAST_EXPECT(val.isConvertibleTo(Json::intValue)); - BEAST_EXPECT(val.isConvertibleTo(Json::uintValue)); - BEAST_EXPECT(val.isConvertibleTo(Json::realValue)); - BEAST_EXPECT(val.isConvertibleTo(Json::stringValue)); - BEAST_EXPECT(val.isConvertibleTo(Json::booleanValue)); - BEAST_EXPECT(!val.isConvertibleTo(Json::arrayValue)); - BEAST_EXPECT(!val.isConvertibleTo(Json::objectValue)); - } - { - // array type - Json::Value val(Json::arrayValue); - BEAST_EXPECT(val.isArray()); - // BEAST_EXPECT(strcmp (val.asCString(), ?) == 0); // - // asserts BEAST_EXPECT(val.asString() == ?); // asserts or - // throws BEAST_EXPECT(val.asInt() == ?); // asserts or - // throws BEAST_EXPECT(val.asUInt() == ?); // asserts or - // throws BEAST_EXPECT(val.asDouble() == ?); // asserts or - // throws - BEAST_EXPECT(val.asBool() == false); // empty or not - - BEAST_EXPECT(val.isConvertibleTo(Json::nullValue)); - BEAST_EXPECT(!val.isConvertibleTo(Json::intValue)); - BEAST_EXPECT(!val.isConvertibleTo(Json::uintValue)); - BEAST_EXPECT(!val.isConvertibleTo(Json::realValue)); - BEAST_EXPECT(!val.isConvertibleTo(Json::stringValue)); - BEAST_EXPECT(!val.isConvertibleTo(Json::booleanValue)); - BEAST_EXPECT(val.isConvertibleTo(Json::arrayValue)); - BEAST_EXPECT(!val.isConvertibleTo(Json::objectValue)); - } - { - // object type - Json::Value val(Json::objectValue); - BEAST_EXPECT(val.isObject()); - // BEAST_EXPECT(strcmp (val.asCString(), ?) == 0); // - // asserts BEAST_EXPECT(strcmp (val.asCString(), ?) == 0); - // // asserts BEAST_EXPECT(val.asString() == ?); // asserts - // or throws BEAST_EXPECT(val.asInt() == ?); // asserts or - // throws BEAST_EXPECT(val.asUInt() == ?); // asserts or - // throws - BEAST_EXPECT(val.asBool() == false); // empty or not - - BEAST_EXPECT(val.isConvertibleTo(Json::nullValue)); - BEAST_EXPECT(!val.isConvertibleTo(Json::intValue)); - BEAST_EXPECT(!val.isConvertibleTo(Json::uintValue)); - BEAST_EXPECT(!val.isConvertibleTo(Json::realValue)); - BEAST_EXPECT(!val.isConvertibleTo(Json::stringValue)); - BEAST_EXPECT(!val.isConvertibleTo(Json::booleanValue)); - BEAST_EXPECT(!val.isConvertibleTo(Json::arrayValue)); - BEAST_EXPECT(val.isConvertibleTo(Json::objectValue)); - } - } - - void - test_access() - { - Json::Value val; - BEAST_EXPECT(val.type() == Json::nullValue); - BEAST_EXPECT(val.size() == 0); - BEAST_EXPECT(!val.isValidIndex(0)); - BEAST_EXPECT(!val.isMember("key")); - { - Json::Value const constVal = val; - BEAST_EXPECT(constVal[7u].type() == Json::nullValue); - BEAST_EXPECT(!constVal.isMember("key")); - BEAST_EXPECT(constVal["key"].type() == Json::nullValue); - BEAST_EXPECT(constVal.getMemberNames().empty()); - BEAST_EXPECT(constVal.get(1u, "default0") == "default0"); - BEAST_EXPECT(constVal.get(std::string("not"), "oh") == "oh"); - BEAST_EXPECT(constVal.get("missing", "default2") == "default2"); - } - - val = -7; - BEAST_EXPECT(val.type() == Json::intValue); - BEAST_EXPECT(val.size() == 0); - BEAST_EXPECT(!val.isValidIndex(0)); - BEAST_EXPECT(!val.isMember("key")); - - val = 42u; - BEAST_EXPECT(val.type() == Json::uintValue); - BEAST_EXPECT(val.size() == 0); - BEAST_EXPECT(!val.isValidIndex(0)); - BEAST_EXPECT(!val.isMember("key")); - - val = 3.14159; - BEAST_EXPECT(val.type() == Json::realValue); - BEAST_EXPECT(val.size() == 0); - BEAST_EXPECT(!val.isValidIndex(0)); - BEAST_EXPECT(!val.isMember("key")); - - val = true; - BEAST_EXPECT(val.type() == Json::booleanValue); - BEAST_EXPECT(val.size() == 0); - BEAST_EXPECT(!val.isValidIndex(0)); - BEAST_EXPECT(!val.isMember("key")); - - val = "string"; - BEAST_EXPECT(val.type() == Json::stringValue); - BEAST_EXPECT(val.size() == 0); - BEAST_EXPECT(!val.isValidIndex(0)); - BEAST_EXPECT(!val.isMember("key")); - - val = Json::Value(Json::objectValue); - BEAST_EXPECT(val.type() == Json::objectValue); - BEAST_EXPECT(val.size() == 0); - static Json::StaticString const staticThree("three"); - val[staticThree] = 3; - val["two"] = 2; - BEAST_EXPECT(val.size() == 2); - BEAST_EXPECT(val.isValidIndex(1)); - BEAST_EXPECT(!val.isValidIndex(2)); - BEAST_EXPECT(val[staticThree] == 3); - BEAST_EXPECT(val.isMember("two")); - BEAST_EXPECT(val.isMember(staticThree)); - BEAST_EXPECT(!val.isMember("key")); - { - Json::Value const constVal = val; - BEAST_EXPECT(constVal["two"] == 2); - BEAST_EXPECT(constVal["four"].type() == Json::nullValue); - BEAST_EXPECT(constVal[staticThree] == 3); - BEAST_EXPECT(constVal.isMember("two")); - BEAST_EXPECT(constVal.isMember(staticThree)); - BEAST_EXPECT(!constVal.isMember("key")); - BEAST_EXPECT(val.get(std::string("two"), "backup") == 2); - BEAST_EXPECT(val.get("missing", "default2") == "default2"); - } - - val = Json::Value(Json::arrayValue); - BEAST_EXPECT(val.type() == Json::arrayValue); - BEAST_EXPECT(val.size() == 0); - val[0u] = "zero"; - val[1u] = "one"; - BEAST_EXPECT(val.size() == 2); - BEAST_EXPECT(val.isValidIndex(1)); - BEAST_EXPECT(!val.isValidIndex(2)); - BEAST_EXPECT(val[20u].type() == Json::nullValue); - BEAST_EXPECT(!val.isMember("key")); - { - Json::Value const constVal = val; - BEAST_EXPECT(constVal[0u] == "zero"); - BEAST_EXPECT(constVal[2u].type() == Json::nullValue); - BEAST_EXPECT(!constVal.isMember("key")); - BEAST_EXPECT(val.get(1u, "default0") == "one"); - BEAST_EXPECT(val.get(3u, "default1") == "default1"); - } - } - - void - test_removeMember() - { - Json::Value val; - BEAST_EXPECT( - val.removeMember(std::string("member")).type() == Json::nullValue); - - val = Json::Value(Json::objectValue); - static Json::StaticString const staticThree("three"); - val[staticThree] = 3; - val["two"] = 2; - BEAST_EXPECT(val.size() == 2); - - BEAST_EXPECT( - val.removeMember(std::string("six")).type() == Json::nullValue); - BEAST_EXPECT(val.size() == 2); - - BEAST_EXPECT(val.removeMember(staticThree) == 3); - BEAST_EXPECT(val.size() == 1); - - BEAST_EXPECT(val.removeMember(staticThree).type() == Json::nullValue); - BEAST_EXPECT(val.size() == 1); - - BEAST_EXPECT(val.removeMember(std::string("two")) == 2); - BEAST_EXPECT(val.size() == 0); - - BEAST_EXPECT( - val.removeMember(std::string("two")).type() == Json::nullValue); - BEAST_EXPECT(val.size() == 0); - } - - void - test_iterator() - { - { - // Iterating an array. - Json::Value arr{Json::arrayValue}; - arr[0u] = "zero"; - arr[1u] = "one"; - arr[2u] = "two"; - arr[3u] = "three"; - - Json::ValueIterator const b{arr.begin()}; - Json::ValueIterator const e{arr.end()}; - - Json::ValueIterator i1 = b; - Json::ValueIterator i2 = e; - --i2; - - // key(), index(), and memberName() on an object iterator. - BEAST_EXPECT(b != e); - BEAST_EXPECT(!(b == e)); - BEAST_EXPECT(i1.key() == 0); - BEAST_EXPECT(i2.key() == 3); - BEAST_EXPECT(i1.index() == 0); - BEAST_EXPECT(i2.index() == 3); - BEAST_EXPECT(std::strcmp(i1.memberName(), "") == 0); - BEAST_EXPECT(std::strcmp(i2.memberName(), "") == 0); - - // Pre and post increment and decrement. - *i1++ = "0"; - BEAST_EXPECT(*i1 == "one"); - *i1 = "1"; - ++i1; - - *i2-- = "3"; - BEAST_EXPECT(*i2 == "two"); - BEAST_EXPECT(i1 == i2); - *i2 = "2"; - BEAST_EXPECT(*i1 == "2"); - } - { - // Iterating a const object. - Json::Value const obj{[]() { - Json::Value obj{Json::objectValue}; - obj["0"] = 0; - obj["1"] = 1; - obj["2"] = 2; - obj["3"] = 3; - return obj; - }()}; - - Json::ValueConstIterator i1{obj.begin()}; - Json::ValueConstIterator i2{obj.end()}; - --i2; - - // key(), index(), and memberName() on an object iterator. - BEAST_EXPECT(i1 != i2); - BEAST_EXPECT(!(i1 == i2)); - BEAST_EXPECT(i1.key() == "0"); - BEAST_EXPECT(i2.key() == "3"); - BEAST_EXPECT(i1.index() == -1); - BEAST_EXPECT(i2.index() == -1); - BEAST_EXPECT(std::strcmp(i1.memberName(), "0") == 0); - BEAST_EXPECT(std::strcmp(i2.memberName(), "3") == 0); - - // Pre and post increment and decrement. - BEAST_EXPECT(*i1++ == 0); - BEAST_EXPECT(*i1 == 1); - ++i1; - - BEAST_EXPECT(*i2-- == 3); - BEAST_EXPECT(*i2 == 2); - BEAST_EXPECT(i1 == i2); - BEAST_EXPECT(*i1 == 2); - } - { - // Iterating a non-const null object. - Json::Value nul{}; - BEAST_EXPECT(nul.begin() == nul.end()); - } - { - // Iterating a const Int. - Json::Value const i{-3}; - BEAST_EXPECT(i.begin() == i.end()); - } - } - - void - test_nest_limits() - { - Json::Reader r; - { - auto nest = [](std::uint32_t depth) -> std::string { - std::string s = "{"; - for (std::uint32_t i{1}; i <= depth; ++i) - s += "\"obj\":{"; - for (std::uint32_t i{1}; i <= depth; ++i) - s += "}"; - s += "}"; - return s; - }; - - { - // Within object nest limit - auto json{nest(std::min(10u, Json::Reader::nest_limit))}; - Json::Value j; - BEAST_EXPECT(r.parse(json, j)); - } - - { - // Exceed object nest limit - auto json{nest(Json::Reader::nest_limit + 1)}; - Json::Value j; - BEAST_EXPECT(!r.parse(json, j)); - } - } - - auto nest = [](std::uint32_t depth) -> std::string { - std::string s = "{"; - for (std::uint32_t i{1}; i <= depth; ++i) - s += "\"array\":[{"; - for (std::uint32_t i{1}; i <= depth; ++i) - s += "]}"; - s += "}"; - return s; - }; - { - // Exceed array nest limit - auto json{nest(Json::Reader::nest_limit + 1)}; - Json::Value j; - BEAST_EXPECT(!r.parse(json, j)); - } - } - - void - test_leak() - { - // When run with the address sanitizer, this test confirms there is no - // memory leak with the scenarios below. - { - Json::Value a; - a[0u] = 1; - BEAST_EXPECT(a.type() == Json::arrayValue); - BEAST_EXPECT(a[0u].type() == Json::intValue); - a = std::move(a[0u]); - BEAST_EXPECT(a.type() == Json::intValue); - } - { - Json::Value b; - Json::Value temp; - temp["a"] = "Probably avoids the small string optimization"; - temp["b"] = "Also probably avoids the small string optimization"; - BEAST_EXPECT(temp.type() == Json::objectValue); - b.append(temp); - BEAST_EXPECT(temp.type() == Json::objectValue); - BEAST_EXPECT(b.size() == 1); - - b.append(std::move(temp)); - BEAST_EXPECT(b.size() == 2); - - // Note that the type() == nullValue check is implementation - // specific and not guaranteed to be valid in the future. - BEAST_EXPECT(temp.type() == Json::nullValue); - } - } - - void - run() override - { - test_StaticString(); - test_types(); - test_compare(); - test_bool(); - test_bad_json(); - test_edge_cases(); - test_copy(); - test_move(); - test_comparisons(); - test_compact(); - test_conversions(); - test_access(); - test_removeMember(); - test_iterator(); - test_nest_limits(); - test_leak(); - } -}; - -BEAST_DEFINE_TESTSUITE(json_value, json, ripple); - -} // namespace ripple diff --git a/src/tests/libxrpl/CMakeLists.txt b/src/tests/libxrpl/CMakeLists.txt index f97283c955..caa308dd1f 100644 --- a/src/tests/libxrpl/CMakeLists.txt +++ b/src/tests/libxrpl/CMakeLists.txt @@ -12,5 +12,7 @@ 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) +xrpl_add_test(json) +target_link_libraries(xrpl.test.json PRIVATE xrpl.imports.test) xrpl_add_test(net) target_link_libraries(xrpl.test.net PRIVATE xrpl.imports.test) diff --git a/src/test/json/Output_test.cpp b/src/tests/libxrpl/json/Output.cpp similarity index 51% rename from src/test/json/Output_test.cpp rename to src/tests/libxrpl/json/Output.cpp index 6421682b01..3b542d81b0 100644 --- a/src/test/json/Output_test.cpp +++ b/src/tests/libxrpl/json/Output.cpp @@ -17,50 +17,43 @@ */ //============================================================================== -#include - +#include #include #include -namespace Json { +#include -struct Output_test : ripple::test::TestOutputSuite +#include + +using namespace ripple; +using namespace Json; + +TEST_SUITE_BEGIN("JsonOutput"); + +static void +checkOutput(std::string const& valueDesc) { - void - runTest(std::string const& name, std::string const& valueDesc) - { - setup(name); - Json::Value value; - BEAST_EXPECT(Json::Reader().parse(valueDesc, value)); - auto out = stringOutput(output_); - outputJson(value, out); + std::string output; + Json::Value value; + REQUIRE(Json::Reader().parse(valueDesc, value)); + auto out = stringOutput(output); + outputJson(value, out); - // Compare with the original version. - auto expected = Json::FastWriter().write(value); - expectResult(expected); - expectResult(valueDesc); - expectResult(jsonAsString(value)); - } + auto expected = Json::FastWriter().write(value); + CHECK(output == expected); + CHECK(output == valueDesc); + CHECK(output == jsonAsString(value)); +} - void - runTest(std::string const& name) - { - runTest(name, name); - } +TEST_CASE("output cases") +{ + checkOutput("{}"); + checkOutput("[]"); + checkOutput(R"([23,4.25,true,null,"string"])"); + checkOutput(R"({"hello":"world"})"); + checkOutput("[{}]"); + checkOutput("[[]]"); + checkOutput(R"({"array":[{"12":23},{},null,false,0.5]})"); +} - void - run() override - { - runTest("empty dict", "{}"); - runTest("empty array", "[]"); - runTest("array", "[23,4.25,true,null,\"string\"]"); - runTest("dict", "{\"hello\":\"world\"}"); - runTest("array dict", "[{}]"); - runTest("array array", "[[]]"); - runTest("more complex", "{\"array\":[{\"12\":23},{},null,false,0.5]}"); - } -}; - -BEAST_DEFINE_TESTSUITE(Output, json, ripple); - -} // namespace Json +TEST_SUITE_END(); diff --git a/src/tests/libxrpl/json/Value.cpp b/src/tests/libxrpl/json/Value.cpp new file mode 100644 index 0000000000..3da8e14ba4 --- /dev/null +++ b/src/tests/libxrpl/json/Value.cpp @@ -0,0 +1,1294 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include +#include +#include + +#include + +#include +#include +#include +#include + +namespace ripple { + +TEST_SUITE_BEGIN("json_value"); + +TEST_CASE("construct and compare Json::StaticString") +{ + static constexpr char sample[]{"Contents of a Json::StaticString"}; + + static constexpr Json::StaticString test1(sample); + char const* addrTest1{test1}; + + CHECK(addrTest1 == &sample[0]); + CHECK(test1.c_str() == &sample[0]); + + static constexpr Json::StaticString test2{ + "Contents of a Json::StaticString"}; + static constexpr Json::StaticString test3{"Another StaticString"}; + + CHECK(test1 == test2); + CHECK(test1 != test3); + + std::string str{sample}; + CHECK(str == test2); + CHECK(str != test3); + CHECK(test2 == str); + CHECK(test3 != str); +} + +TEST_CASE("different types") +{ + // Exercise ValueType constructor + static constexpr Json::StaticString staticStr{"staticStr"}; + + auto testCopy = [](Json::ValueType typ) { + Json::Value val{typ}; + Json::Value cpy{val}; + CHECK(val.type() == typ); + CHECK(cpy.type() == typ); + return val; + }; + { + Json::Value const nullV{testCopy(Json::nullValue)}; + CHECK(nullV.isNull()); + CHECK(!nullV.isBool()); + CHECK(!nullV.isInt()); + CHECK(!nullV.isUInt()); + CHECK(!nullV.isIntegral()); + CHECK(!nullV.isDouble()); + CHECK(!nullV.isNumeric()); + CHECK(!nullV.isString()); + CHECK(!nullV.isArray()); + CHECK(nullV.isArrayOrNull()); + CHECK(!nullV.isObject()); + CHECK(nullV.isObjectOrNull()); + } + { + Json::Value const intV{testCopy(Json::intValue)}; + CHECK(!intV.isNull()); + CHECK(!intV.isBool()); + CHECK(intV.isInt()); + CHECK(!intV.isUInt()); + CHECK(intV.isIntegral()); + CHECK(!intV.isDouble()); + CHECK(intV.isNumeric()); + CHECK(!intV.isString()); + CHECK(!intV.isArray()); + CHECK(!intV.isArrayOrNull()); + CHECK(!intV.isObject()); + CHECK(!intV.isObjectOrNull()); + } + { + Json::Value const uintV{testCopy(Json::uintValue)}; + CHECK(!uintV.isNull()); + CHECK(!uintV.isBool()); + CHECK(!uintV.isInt()); + CHECK(uintV.isUInt()); + CHECK(uintV.isIntegral()); + CHECK(!uintV.isDouble()); + CHECK(uintV.isNumeric()); + CHECK(!uintV.isString()); + CHECK(!uintV.isArray()); + CHECK(!uintV.isArrayOrNull()); + CHECK(!uintV.isObject()); + CHECK(!uintV.isObjectOrNull()); + } + { + Json::Value const realV{testCopy(Json::realValue)}; + CHECK(!realV.isNull()); + CHECK(!realV.isBool()); + CHECK(!realV.isInt()); + CHECK(!realV.isUInt()); + CHECK(!realV.isIntegral()); + CHECK(realV.isDouble()); + CHECK(realV.isNumeric()); + CHECK(!realV.isString()); + CHECK(!realV.isArray()); + CHECK(!realV.isArrayOrNull()); + CHECK(!realV.isObject()); + CHECK(!realV.isObjectOrNull()); + } + { + Json::Value const stringV{testCopy(Json::stringValue)}; + CHECK(!stringV.isNull()); + CHECK(!stringV.isBool()); + CHECK(!stringV.isInt()); + CHECK(!stringV.isUInt()); + CHECK(!stringV.isIntegral()); + CHECK(!stringV.isDouble()); + CHECK(!stringV.isNumeric()); + CHECK(stringV.isString()); + CHECK(!stringV.isArray()); + CHECK(!stringV.isArrayOrNull()); + CHECK(!stringV.isObject()); + CHECK(!stringV.isObjectOrNull()); + } + { + Json::Value const staticStrV{staticStr}; + { + Json::Value cpy{staticStrV}; + CHECK(staticStrV.type() == Json::stringValue); + CHECK(cpy.type() == Json::stringValue); + } + CHECK(!staticStrV.isNull()); + CHECK(!staticStrV.isBool()); + CHECK(!staticStrV.isInt()); + CHECK(!staticStrV.isUInt()); + CHECK(!staticStrV.isIntegral()); + CHECK(!staticStrV.isDouble()); + CHECK(!staticStrV.isNumeric()); + CHECK(staticStrV.isString()); + CHECK(!staticStrV.isArray()); + CHECK(!staticStrV.isArrayOrNull()); + CHECK(!staticStrV.isObject()); + CHECK(!staticStrV.isObjectOrNull()); + } + { + Json::Value const boolV{testCopy(Json::booleanValue)}; + CHECK(!boolV.isNull()); + CHECK(boolV.isBool()); + CHECK(!boolV.isInt()); + CHECK(!boolV.isUInt()); + CHECK(boolV.isIntegral()); + CHECK(!boolV.isDouble()); + CHECK(boolV.isNumeric()); + CHECK(!boolV.isString()); + CHECK(!boolV.isArray()); + CHECK(!boolV.isArrayOrNull()); + CHECK(!boolV.isObject()); + CHECK(!boolV.isObjectOrNull()); + } + { + Json::Value const arrayV{testCopy(Json::arrayValue)}; + CHECK(!arrayV.isNull()); + CHECK(!arrayV.isBool()); + CHECK(!arrayV.isInt()); + CHECK(!arrayV.isUInt()); + CHECK(!arrayV.isIntegral()); + CHECK(!arrayV.isDouble()); + CHECK(!arrayV.isNumeric()); + CHECK(!arrayV.isString()); + CHECK(arrayV.isArray()); + CHECK(arrayV.isArrayOrNull()); + CHECK(!arrayV.isObject()); + CHECK(!arrayV.isObjectOrNull()); + } + { + Json::Value const objectV{testCopy(Json::objectValue)}; + CHECK(!objectV.isNull()); + CHECK(!objectV.isBool()); + CHECK(!objectV.isInt()); + CHECK(!objectV.isUInt()); + CHECK(!objectV.isIntegral()); + CHECK(!objectV.isDouble()); + CHECK(!objectV.isNumeric()); + CHECK(!objectV.isString()); + CHECK(!objectV.isArray()); + CHECK(!objectV.isArrayOrNull()); + CHECK(objectV.isObject()); + CHECK(objectV.isObjectOrNull()); + } +} + +TEST_CASE("compare strings") +{ + auto doCompare = [&](Json::Value const& lhs, + Json::Value const& rhs, + bool lhsEqRhs, + bool lhsLtRhs, + int line) { + CAPTURE(line); + CHECK((lhs == rhs) == lhsEqRhs); + CHECK((lhs != rhs) != lhsEqRhs); + CHECK((lhs < rhs) == (!(lhsEqRhs || !lhsLtRhs))); + CHECK((lhs <= rhs) == (lhsEqRhs || lhsLtRhs)); + CHECK((lhs >= rhs) == (lhsEqRhs || !lhsLtRhs)); + CHECK((lhs > rhs) == (!(lhsEqRhs || lhsLtRhs))); + }; + + Json::Value const null0; + Json::Value const intNeg1{-1}; + Json::Value const int0{Json::intValue}; + Json::Value const intPos1{1}; + Json::Value const uint0{Json::uintValue}; + Json::Value const uint1{1u}; + Json::Value const realNeg1{-1.0}; + Json::Value const real0{Json::realValue}; + Json::Value const realPos1{1.0}; + Json::Value const str0{Json::stringValue}; + Json::Value const str1{"1"}; + Json::Value const boolF{false}; + Json::Value const boolT{true}; + Json::Value const array0{Json::arrayValue}; + Json::Value const array1{[]() { + Json::Value array1; + array1[0u] = 1; + return array1; + }()}; + Json::Value const obj0{Json::objectValue}; + Json::Value const obj1{[]() { + Json::Value obj1; + obj1["one"] = 1; + return obj1; + }()}; + +#pragma push_macro("DO_COMPARE") + // DO_COMPARE(lhs, rhs, lhsEqualsToRhs lhsLessThanRhs) +#define DO_COMPARE(lhs, rhs, eq, lt) doCompare(lhs, rhs, eq, lt, __LINE__) + DO_COMPARE(null0, Json::Value{}, true, false); + DO_COMPARE(null0, intNeg1, false, true); + DO_COMPARE(null0, int0, false, true); + DO_COMPARE(null0, intPos1, false, true); + DO_COMPARE(null0, uint0, false, true); + DO_COMPARE(null0, uint1, false, true); + DO_COMPARE(null0, realNeg1, false, true); + DO_COMPARE(null0, real0, false, true); + DO_COMPARE(null0, realPos1, false, true); + DO_COMPARE(null0, str0, false, true); + DO_COMPARE(null0, str1, false, true); + DO_COMPARE(null0, boolF, false, true); + DO_COMPARE(null0, boolT, false, true); + DO_COMPARE(null0, array0, false, true); + DO_COMPARE(null0, array1, false, true); + DO_COMPARE(null0, obj0, false, true); + DO_COMPARE(null0, obj1, false, true); + + DO_COMPARE(intNeg1, null0, false, false); + DO_COMPARE(intNeg1, intNeg1, true, false); + DO_COMPARE(intNeg1, int0, false, true); + DO_COMPARE(intNeg1, intPos1, false, true); + DO_COMPARE(intNeg1, uint0, false, true); + DO_COMPARE(intNeg1, uint1, false, true); + DO_COMPARE(intNeg1, realNeg1, false, true); + DO_COMPARE(intNeg1, real0, false, true); + DO_COMPARE(intNeg1, realPos1, false, true); + DO_COMPARE(intNeg1, str0, false, true); + DO_COMPARE(intNeg1, str1, false, true); + DO_COMPARE(intNeg1, boolF, false, true); + DO_COMPARE(intNeg1, boolT, false, true); + DO_COMPARE(intNeg1, array0, false, true); + DO_COMPARE(intNeg1, array1, false, true); + DO_COMPARE(intNeg1, obj0, false, true); + DO_COMPARE(intNeg1, obj1, false, true); + + DO_COMPARE(int0, null0, false, false); + DO_COMPARE(int0, intNeg1, false, false); + DO_COMPARE(int0, int0, true, false); + DO_COMPARE(int0, intPos1, false, true); + DO_COMPARE(int0, uint0, true, false); + DO_COMPARE(int0, uint1, false, true); + DO_COMPARE(int0, realNeg1, false, true); + DO_COMPARE(int0, real0, false, true); + DO_COMPARE(int0, realPos1, false, true); + DO_COMPARE(int0, str0, false, true); + DO_COMPARE(int0, str1, false, true); + DO_COMPARE(int0, boolF, false, true); + DO_COMPARE(int0, boolT, false, true); + DO_COMPARE(int0, array0, false, true); + DO_COMPARE(int0, array1, false, true); + DO_COMPARE(int0, obj0, false, true); + DO_COMPARE(int0, obj1, false, true); + + DO_COMPARE(intPos1, null0, false, false); + DO_COMPARE(intPos1, intNeg1, false, false); + DO_COMPARE(intPos1, int0, false, false); + DO_COMPARE(intPos1, intPos1, true, false); + DO_COMPARE(intPos1, uint0, false, false); + DO_COMPARE(intPos1, uint1, true, false); + DO_COMPARE(intPos1, realNeg1, false, true); + DO_COMPARE(intPos1, real0, false, true); + DO_COMPARE(intPos1, realPos1, false, true); + DO_COMPARE(intPos1, str0, false, true); + DO_COMPARE(intPos1, str1, false, true); + DO_COMPARE(intPos1, boolF, false, true); + DO_COMPARE(intPos1, boolT, false, true); + DO_COMPARE(intPos1, array0, false, true); + DO_COMPARE(intPos1, array1, false, true); + DO_COMPARE(intPos1, obj0, false, true); + DO_COMPARE(intPos1, obj1, false, true); + + DO_COMPARE(uint0, null0, false, false); + DO_COMPARE(uint0, intNeg1, false, false); + DO_COMPARE(uint0, int0, true, false); + DO_COMPARE(uint0, intPos1, false, true); + DO_COMPARE(uint0, uint0, true, false); + DO_COMPARE(uint0, uint1, false, true); + DO_COMPARE(uint0, realNeg1, false, true); + DO_COMPARE(uint0, real0, false, true); + DO_COMPARE(uint0, realPos1, false, true); + DO_COMPARE(uint0, str0, false, true); + DO_COMPARE(uint0, str1, false, true); + DO_COMPARE(uint0, boolF, false, true); + DO_COMPARE(uint0, boolT, false, true); + DO_COMPARE(uint0, array0, false, true); + DO_COMPARE(uint0, array1, false, true); + DO_COMPARE(uint0, obj0, false, true); + DO_COMPARE(uint0, obj1, false, true); + + DO_COMPARE(uint1, null0, false, false); + DO_COMPARE(uint1, intNeg1, false, false); + DO_COMPARE(uint1, int0, false, false); + DO_COMPARE(uint1, intPos1, true, false); + DO_COMPARE(uint1, uint0, false, false); + DO_COMPARE(uint1, uint1, true, false); + DO_COMPARE(uint1, realNeg1, false, true); + DO_COMPARE(uint1, real0, false, true); + DO_COMPARE(uint1, realPos1, false, true); + DO_COMPARE(uint1, str0, false, true); + DO_COMPARE(uint1, str1, false, true); + DO_COMPARE(uint1, boolF, false, true); + DO_COMPARE(uint1, boolT, false, true); + DO_COMPARE(uint1, array0, false, true); + DO_COMPARE(uint1, array1, false, true); + DO_COMPARE(uint1, obj0, false, true); + DO_COMPARE(uint1, obj1, false, true); + + DO_COMPARE(realNeg1, null0, false, false); + DO_COMPARE(realNeg1, intNeg1, false, false); + DO_COMPARE(realNeg1, int0, false, false); + DO_COMPARE(realNeg1, intPos1, false, false); + DO_COMPARE(realNeg1, uint0, false, false); + DO_COMPARE(realNeg1, uint1, false, false); + DO_COMPARE(realNeg1, realNeg1, true, false); + DO_COMPARE(realNeg1, real0, false, true); + DO_COMPARE(realNeg1, realPos1, false, true); + DO_COMPARE(realNeg1, str0, false, true); + DO_COMPARE(realNeg1, str1, false, true); + DO_COMPARE(realNeg1, boolF, false, true); + DO_COMPARE(realNeg1, boolT, false, true); + DO_COMPARE(realNeg1, array0, false, true); + DO_COMPARE(realNeg1, array1, false, true); + DO_COMPARE(realNeg1, obj0, false, true); + DO_COMPARE(realNeg1, obj1, false, true); + + DO_COMPARE(real0, null0, false, false); + DO_COMPARE(real0, intNeg1, false, false); + DO_COMPARE(real0, int0, false, false); + DO_COMPARE(real0, intPos1, false, false); + DO_COMPARE(real0, uint0, false, false); + DO_COMPARE(real0, uint1, false, false); + DO_COMPARE(real0, realNeg1, false, false); + DO_COMPARE(real0, real0, true, false); + DO_COMPARE(real0, realPos1, false, true); + DO_COMPARE(real0, str0, false, true); + DO_COMPARE(real0, str1, false, true); + DO_COMPARE(real0, boolF, false, true); + DO_COMPARE(real0, boolT, false, true); + DO_COMPARE(real0, array0, false, true); + DO_COMPARE(real0, array1, false, true); + DO_COMPARE(real0, obj0, false, true); + DO_COMPARE(real0, obj1, false, true); + + DO_COMPARE(realPos1, null0, false, false); + DO_COMPARE(realPos1, intNeg1, false, false); + DO_COMPARE(realPos1, int0, false, false); + DO_COMPARE(realPos1, intPos1, false, false); + DO_COMPARE(realPos1, uint0, false, false); + DO_COMPARE(realPos1, uint1, false, false); + DO_COMPARE(realPos1, realNeg1, false, false); + DO_COMPARE(realPos1, real0, false, false); + DO_COMPARE(realPos1, realPos1, true, false); + DO_COMPARE(realPos1, str0, false, true); + DO_COMPARE(realPos1, str1, false, true); + DO_COMPARE(realPos1, boolF, false, true); + DO_COMPARE(realPos1, boolT, false, true); + DO_COMPARE(realPos1, array0, false, true); + DO_COMPARE(realPos1, array1, false, true); + DO_COMPARE(realPos1, obj0, false, true); + DO_COMPARE(realPos1, obj1, false, true); + + DO_COMPARE(str0, null0, false, false); + DO_COMPARE(str0, intNeg1, false, false); + DO_COMPARE(str0, int0, false, false); + DO_COMPARE(str0, intPos1, false, false); + DO_COMPARE(str0, uint0, false, false); + DO_COMPARE(str0, uint1, false, false); + DO_COMPARE(str0, realNeg1, false, false); + DO_COMPARE(str0, real0, false, false); + DO_COMPARE(str0, realPos1, false, false); + DO_COMPARE(str0, str0, true, false); + DO_COMPARE(str0, str1, false, true); + DO_COMPARE(str0, boolF, false, true); + DO_COMPARE(str0, boolT, false, true); + DO_COMPARE(str0, array0, false, true); + DO_COMPARE(str0, array1, false, true); + DO_COMPARE(str0, obj0, false, true); + DO_COMPARE(str0, obj1, false, true); + + DO_COMPARE(str1, null0, false, false); + DO_COMPARE(str1, intNeg1, false, false); + DO_COMPARE(str1, int0, false, false); + DO_COMPARE(str1, intPos1, false, false); + DO_COMPARE(str1, uint0, false, false); + DO_COMPARE(str1, uint1, false, false); + DO_COMPARE(str1, realNeg1, false, false); + DO_COMPARE(str1, real0, false, false); + DO_COMPARE(str1, realPos1, false, false); + DO_COMPARE(str1, str0, false, false); + DO_COMPARE(str1, str1, true, false); + DO_COMPARE(str1, boolF, false, true); + DO_COMPARE(str1, boolT, false, true); + DO_COMPARE(str1, array0, false, true); + DO_COMPARE(str1, array1, false, true); + DO_COMPARE(str1, obj0, false, true); + DO_COMPARE(str1, obj1, false, true); + + DO_COMPARE(boolF, null0, false, false); + DO_COMPARE(boolF, intNeg1, false, false); + DO_COMPARE(boolF, int0, false, false); + DO_COMPARE(boolF, intPos1, false, false); + DO_COMPARE(boolF, uint0, false, false); + DO_COMPARE(boolF, uint1, false, false); + DO_COMPARE(boolF, realNeg1, false, false); + DO_COMPARE(boolF, real0, false, false); + DO_COMPARE(boolF, realPos1, false, false); + DO_COMPARE(boolF, str0, false, false); + DO_COMPARE(boolF, str1, false, false); + DO_COMPARE(boolF, boolF, true, false); + DO_COMPARE(boolF, boolT, false, true); + DO_COMPARE(boolF, array0, false, true); + DO_COMPARE(boolF, array1, false, true); + DO_COMPARE(boolF, obj0, false, true); + DO_COMPARE(boolF, obj1, false, true); + + DO_COMPARE(boolT, null0, false, false); + DO_COMPARE(boolT, intNeg1, false, false); + DO_COMPARE(boolT, int0, false, false); + DO_COMPARE(boolT, intPos1, false, false); + DO_COMPARE(boolT, uint0, false, false); + DO_COMPARE(boolT, uint1, false, false); + DO_COMPARE(boolT, realNeg1, false, false); + DO_COMPARE(boolT, real0, false, false); + DO_COMPARE(boolT, realPos1, false, false); + DO_COMPARE(boolT, str0, false, false); + DO_COMPARE(boolT, str1, false, false); + DO_COMPARE(boolT, boolF, false, false); + DO_COMPARE(boolT, boolT, true, false); + DO_COMPARE(boolT, array0, false, true); + DO_COMPARE(boolT, array1, false, true); + DO_COMPARE(boolT, obj0, false, true); + DO_COMPARE(boolT, obj1, false, true); + + DO_COMPARE(array0, null0, false, false); + DO_COMPARE(array0, intNeg1, false, false); + DO_COMPARE(array0, int0, false, false); + DO_COMPARE(array0, intPos1, false, false); + DO_COMPARE(array0, uint0, false, false); + DO_COMPARE(array0, uint1, false, false); + DO_COMPARE(array0, realNeg1, false, false); + DO_COMPARE(array0, real0, false, false); + DO_COMPARE(array0, realPos1, false, false); + DO_COMPARE(array0, str0, false, false); + DO_COMPARE(array0, str1, false, false); + DO_COMPARE(array0, boolF, false, false); + DO_COMPARE(array0, boolT, false, false); + DO_COMPARE(array0, array0, true, false); + DO_COMPARE(array0, array1, false, true); + DO_COMPARE(array0, obj0, false, true); + DO_COMPARE(array0, obj1, false, true); + + DO_COMPARE(array1, null0, false, false); + DO_COMPARE(array1, intNeg1, false, false); + DO_COMPARE(array1, int0, false, false); + DO_COMPARE(array1, intPos1, false, false); + DO_COMPARE(array1, uint0, false, false); + DO_COMPARE(array1, uint1, false, false); + DO_COMPARE(array1, realNeg1, false, false); + DO_COMPARE(array1, real0, false, false); + DO_COMPARE(array1, realPos1, false, false); + DO_COMPARE(array1, str0, false, false); + DO_COMPARE(array1, str1, false, false); + DO_COMPARE(array1, boolF, false, false); + DO_COMPARE(array1, boolT, false, false); + DO_COMPARE(array1, array0, false, false); + DO_COMPARE(array1, array1, true, false); + DO_COMPARE(array1, obj0, false, true); + DO_COMPARE(array1, obj1, false, true); + + DO_COMPARE(obj0, null0, false, false); + DO_COMPARE(obj0, intNeg1, false, false); + DO_COMPARE(obj0, int0, false, false); + DO_COMPARE(obj0, intPos1, false, false); + DO_COMPARE(obj0, uint0, false, false); + DO_COMPARE(obj0, uint1, false, false); + DO_COMPARE(obj0, realNeg1, false, false); + DO_COMPARE(obj0, real0, false, false); + DO_COMPARE(obj0, realPos1, false, false); + DO_COMPARE(obj0, str0, false, false); + DO_COMPARE(obj0, str1, false, false); + DO_COMPARE(obj0, boolF, false, false); + DO_COMPARE(obj0, boolT, false, false); + DO_COMPARE(obj0, array0, false, false); + DO_COMPARE(obj0, array1, false, false); + DO_COMPARE(obj0, obj0, true, false); + DO_COMPARE(obj0, obj1, false, true); + + DO_COMPARE(obj1, null0, false, false); + DO_COMPARE(obj1, intNeg1, false, false); + DO_COMPARE(obj1, int0, false, false); + DO_COMPARE(obj1, intPos1, false, false); + DO_COMPARE(obj1, uint0, false, false); + DO_COMPARE(obj1, uint1, false, false); + DO_COMPARE(obj1, realNeg1, false, false); + DO_COMPARE(obj1, real0, false, false); + DO_COMPARE(obj1, realPos1, false, false); + DO_COMPARE(obj1, str0, false, false); + DO_COMPARE(obj1, str1, false, false); + DO_COMPARE(obj1, boolF, false, false); + DO_COMPARE(obj1, boolT, false, false); + DO_COMPARE(obj1, array0, false, false); + DO_COMPARE(obj1, array1, false, false); + DO_COMPARE(obj1, obj0, false, false); + DO_COMPARE(obj1, obj1, true, false); +#undef DO_COMPARE +#pragma pop_macro("DO_COMPARE") +} + +TEST_CASE("bool") +{ + CHECK(!Json::Value()); + + CHECK(!Json::Value("")); + + CHECK(bool(Json::Value("empty"))); + CHECK(bool(Json::Value(false))); + CHECK(bool(Json::Value(true))); + CHECK(bool(Json::Value(0))); + CHECK(bool(Json::Value(1))); + + Json::Value array(Json::arrayValue); + CHECK(!array); + array.append(0); + CHECK(bool(array)); + + Json::Value object(Json::objectValue); + CHECK(!object); + object[""] = false; + CHECK(bool(object)); +} + +TEST_CASE("bad json") +{ + char const* s(R"({"method":"ledger","params":[{"ledger_index":1e300}]})"); + + Json::Value j; + Json::Reader r; + + CHECK(r.parse(s, j)); +} + +TEST_CASE("edge cases") +{ + std::string json; + + std::uint32_t max_uint = std::numeric_limits::max(); + std::int32_t max_int = std::numeric_limits::max(); + std::int32_t min_int = std::numeric_limits::min(); + + std::uint32_t a_uint = max_uint - 1978; + std::int32_t a_large_int = max_int - 1978; + std::int32_t a_small_int = min_int + 1978; + + json = "{\"max_uint\":" + std::to_string(max_uint); + json += ",\"max_int\":" + std::to_string(max_int); + json += ",\"min_int\":" + std::to_string(min_int); + json += ",\"a_uint\":" + std::to_string(a_uint); + json += ",\"a_large_int\":" + std::to_string(a_large_int); + json += ",\"a_small_int\":" + std::to_string(a_small_int); + json += "}"; + + Json::Value j1; + Json::Reader r1; + + CHECK(r1.parse(json, j1)); + CHECK(j1["max_uint"].asUInt() == max_uint); + CHECK(j1["max_int"].asInt() == max_int); + CHECK(j1["min_int"].asInt() == min_int); + CHECK(j1["a_uint"].asUInt() == a_uint); + CHECK(j1["a_uint"] > a_large_int); + CHECK(j1["a_uint"] > a_small_int); + CHECK(j1["a_large_int"].asInt() == a_large_int); + CHECK(j1["a_large_int"].asUInt() == a_large_int); + CHECK(j1["a_large_int"] < a_uint); + CHECK(j1["a_small_int"].asInt() == a_small_int); + CHECK(j1["a_small_int"] < a_uint); + + json = "{\"overflow\":"; + json += std::to_string(std::uint64_t(max_uint) + 1); + json += "}"; + + Json::Value j2; + Json::Reader r2; + + CHECK(!r2.parse(json, j2)); + + json = "{\"underflow\":"; + json += std::to_string(std::int64_t(min_int) - 1); + json += "}"; + + Json::Value j3; + Json::Reader r3; + + CHECK(!r3.parse(json, j3)); + + Json::Value intString{"4294967296"}; + CHECK_THROWS_AS(intString.asUInt(), beast::BadLexicalCast); + + intString = "4294967295"; + CHECK(intString.asUInt() == 4294967295u); + + intString = "0"; + CHECK(intString.asUInt() == 0); + + intString = "-1"; + CHECK_THROWS_AS(intString.asUInt(), beast::BadLexicalCast); + + intString = "2147483648"; + CHECK_THROWS_AS(intString.asInt(), beast::BadLexicalCast); + + intString = "2147483647"; + CHECK(intString.asInt() == 2147483647); + + intString = "-2147483648"; + CHECK(intString.asInt() == -2147483648LL); // MSVC wants the LL + + intString = "-2147483649"; + CHECK_THROWS_AS(intString.asInt(), beast::BadLexicalCast); +} + +TEST_CASE("copy") +{ + Json::Value v1{2.5}; + CHECK(v1.isDouble()); + CHECK(v1.asDouble() == 2.5); + + Json::Value v2 = v1; + CHECK(v1.isDouble()); + CHECK(v1.asDouble() == 2.5); + CHECK(v2.isDouble()); + CHECK(v2.asDouble() == 2.5); + CHECK(v1 == v2); + + v1 = v2; + CHECK(v1.isDouble()); + CHECK(v1.asDouble() == 2.5); + CHECK(v2.isDouble()); + CHECK(v2.asDouble() == 2.5); + CHECK(v1 == v2); +} + +TEST_CASE("move") +{ + Json::Value v1{2.5}; + CHECK(v1.isDouble()); + CHECK(v1.asDouble() == 2.5); + + Json::Value v2 = std::move(v1); + CHECK(!v1); + CHECK(v2.isDouble()); + CHECK(v2.asDouble() == 2.5); + CHECK(v1 != v2); + + v1 = std::move(v2); + CHECK(v1.isDouble()); + CHECK(v1.asDouble() == 2.5); + CHECK(!v2); + CHECK(v1 != v2); +} + +TEST_CASE("comparisons") +{ + Json::Value a, b; + auto testEquals = [&](std::string const& name) { + CHECK(a == b); + CHECK(a <= b); + CHECK(a >= b); + + CHECK(!(a != b)); + CHECK(!(a < b)); + CHECK(!(a > b)); + + CHECK(b == a); + CHECK(b <= a); + CHECK(b >= a); + + CHECK(!(b != a)); + CHECK(!(b < a)); + CHECK(!(b > a)); + }; + + auto testGreaterThan = [&](std::string const& name) { + CHECK(!(a == b)); + CHECK(!(a <= b)); + CHECK(a >= b); + + CHECK(a != b); + CHECK(!(a < b)); + CHECK(a > b); + + CHECK(!(b == a)); + CHECK(b <= a); + CHECK(!(b >= a)); + + CHECK(b != a); + CHECK(b < a); + CHECK(!(b > a)); + }; + + a["a"] = Json::UInt(0); + b["a"] = Json::Int(0); + testEquals("zero"); + + b["a"] = Json::Int(-1); + testGreaterThan("negative"); + + Json::Int big = std::numeric_limits::max(); + Json::UInt bigger = big; + bigger++; + + a["a"] = bigger; + b["a"] = big; + testGreaterThan("big"); +} + +TEST_CASE("compact") +{ + Json::Value j; + Json::Reader r; + char const* s("{\"array\":[{\"12\":23},{},null,false,0.5]}"); + + auto countLines = [](std::string const& str) { + return 1 + std::count_if(str.begin(), str.end(), [](char c) { + return c == '\n'; + }); + }; + + CHECK(r.parse(s, j)); + { + std::stringstream ss; + ss << j; + CHECK(countLines(ss.str()) > 1); + } + { + std::stringstream ss; + ss << Json::Compact(std::move(j)); + CHECK(countLines(ss.str()) == 1); + } +} + +TEST_CASE("conversions") +{ + // We have Json::Int, but not Json::Double or Json::Real. + // We have Json::Int, Json::Value::Int, and Json::ValueType::intValue. + // We have Json::ValueType::realValue but Json::Value::asDouble. + // TODO: What's the thinking here? + { + // null + Json::Value val; + CHECK(val.isNull()); + // val.asCString() should trigger an assertion failure + CHECK(val.asString() == ""); + CHECK(val.asInt() == 0); + CHECK(val.asUInt() == 0); + CHECK(val.asDouble() == 0.0); + CHECK(val.asBool() == false); + + CHECK(val.isConvertibleTo(Json::nullValue)); + CHECK(val.isConvertibleTo(Json::intValue)); + CHECK(val.isConvertibleTo(Json::uintValue)); + CHECK(val.isConvertibleTo(Json::realValue)); + CHECK(val.isConvertibleTo(Json::stringValue)); + CHECK(val.isConvertibleTo(Json::booleanValue)); + CHECK(val.isConvertibleTo(Json::arrayValue)); + CHECK(val.isConvertibleTo(Json::objectValue)); + } + { + // int + Json::Value val = -1234; + CHECK(val.isInt()); + // val.asCString() should trigger an assertion failure + CHECK(val.asString() == "-1234"); + CHECK(val.asInt() == -1234); + CHECK_THROWS_AS(val.asUInt(), Json::error); + CHECK(val.asDouble() == -1234.0); + CHECK(val.asBool() == true); + + CHECK(!val.isConvertibleTo(Json::nullValue)); + CHECK(val.isConvertibleTo(Json::intValue)); + CHECK(!val.isConvertibleTo(Json::uintValue)); + CHECK(val.isConvertibleTo(Json::realValue)); + CHECK(val.isConvertibleTo(Json::stringValue)); + CHECK(val.isConvertibleTo(Json::booleanValue)); + CHECK(!val.isConvertibleTo(Json::arrayValue)); + CHECK(!val.isConvertibleTo(Json::objectValue)); + } + { + // uint + Json::Value val = 1234U; + CHECK(val.isUInt()); + // val.asCString() should trigger an assertion failure + CHECK(val.asString() == "1234"); + CHECK(val.asInt() == 1234); + CHECK(val.asUInt() == 1234u); + CHECK(val.asDouble() == 1234.0); + CHECK(val.asBool() == true); + + CHECK(!val.isConvertibleTo(Json::nullValue)); + CHECK(val.isConvertibleTo(Json::intValue)); + CHECK(val.isConvertibleTo(Json::uintValue)); + CHECK(val.isConvertibleTo(Json::realValue)); + CHECK(val.isConvertibleTo(Json::stringValue)); + CHECK(val.isConvertibleTo(Json::booleanValue)); + CHECK(!val.isConvertibleTo(Json::arrayValue)); + CHECK(!val.isConvertibleTo(Json::objectValue)); + } + { + // real + Json::Value val = 2.0; + CHECK(val.isDouble()); + // val.asCString() should trigger an assertion failure + CHECK(std::regex_match(val.asString(), std::regex("^2\\.0*$"))); + CHECK(val.asInt() == 2); + CHECK(val.asUInt() == 2u); + CHECK(val.asDouble() == 2.0); + CHECK(val.asBool() == true); + + CHECK(!val.isConvertibleTo(Json::nullValue)); + CHECK(val.isConvertibleTo(Json::intValue)); + CHECK(val.isConvertibleTo(Json::uintValue)); + CHECK(val.isConvertibleTo(Json::realValue)); + CHECK(val.isConvertibleTo(Json::stringValue)); + CHECK(val.isConvertibleTo(Json::booleanValue)); + CHECK(!val.isConvertibleTo(Json::arrayValue)); + CHECK(!val.isConvertibleTo(Json::objectValue)); + } + { + // numeric string + Json::Value val = "54321"; + CHECK(val.isString()); + CHECK(strcmp(val.asCString(), "54321") == 0); + CHECK(val.asString() == "54321"); + CHECK(val.asInt() == 54321); + CHECK(val.asUInt() == 54321u); + CHECK_THROWS_AS(val.asDouble(), Json::error); + CHECK(val.asBool() == true); + + CHECK(!val.isConvertibleTo(Json::nullValue)); + CHECK(!val.isConvertibleTo(Json::intValue)); + CHECK(!val.isConvertibleTo(Json::uintValue)); + CHECK(!val.isConvertibleTo(Json::realValue)); + CHECK(val.isConvertibleTo(Json::stringValue)); + CHECK(!val.isConvertibleTo(Json::booleanValue)); + CHECK(!val.isConvertibleTo(Json::arrayValue)); + CHECK(!val.isConvertibleTo(Json::objectValue)); + } + { + // non-numeric string + Json::Value val(Json::stringValue); + CHECK(val.isString()); + CHECK(val.asCString() == nullptr); + CHECK(val.asString() == ""); + CHECK_THROWS_AS(val.asInt(), std::exception); + CHECK_THROWS_AS(val.asUInt(), std::exception); + CHECK_THROWS_AS(val.asDouble(), std::exception); + CHECK(val.asBool() == false); + + CHECK(val.isConvertibleTo(Json::nullValue)); + CHECK(!val.isConvertibleTo(Json::intValue)); + CHECK(!val.isConvertibleTo(Json::uintValue)); + CHECK(!val.isConvertibleTo(Json::realValue)); + CHECK(val.isConvertibleTo(Json::stringValue)); + CHECK(!val.isConvertibleTo(Json::booleanValue)); + CHECK(!val.isConvertibleTo(Json::arrayValue)); + CHECK(!val.isConvertibleTo(Json::objectValue)); + } + { + // bool false + Json::Value val = false; + CHECK(val.isBool()); + // val.asCString() should trigger an assertion failure + CHECK(val.asString() == "false"); + CHECK(val.asInt() == 0); + CHECK(val.asUInt() == 0); + CHECK(val.asDouble() == 0.0); + CHECK(val.asBool() == false); + + CHECK(val.isConvertibleTo(Json::nullValue)); + CHECK(val.isConvertibleTo(Json::intValue)); + CHECK(val.isConvertibleTo(Json::uintValue)); + CHECK(val.isConvertibleTo(Json::realValue)); + CHECK(val.isConvertibleTo(Json::stringValue)); + CHECK(val.isConvertibleTo(Json::booleanValue)); + CHECK(!val.isConvertibleTo(Json::arrayValue)); + CHECK(!val.isConvertibleTo(Json::objectValue)); + } + { + // bool true + Json::Value val = true; + CHECK(val.isBool()); + // val.asCString() should trigger an assertion failure + CHECK(val.asString() == "true"); + CHECK(val.asInt() == 1); + CHECK(val.asUInt() == 1); + CHECK(val.asDouble() == 1.0); + CHECK(val.asBool() == true); + + CHECK(!val.isConvertibleTo(Json::nullValue)); + CHECK(val.isConvertibleTo(Json::intValue)); + CHECK(val.isConvertibleTo(Json::uintValue)); + CHECK(val.isConvertibleTo(Json::realValue)); + CHECK(val.isConvertibleTo(Json::stringValue)); + CHECK(val.isConvertibleTo(Json::booleanValue)); + CHECK(!val.isConvertibleTo(Json::arrayValue)); + CHECK(!val.isConvertibleTo(Json::objectValue)); + } + { + // array type + Json::Value val(Json::arrayValue); + CHECK(val.isArray()); + // val.asCString should trigger an assertion failure + CHECK_THROWS_AS(val.asString(), Json::error); + CHECK_THROWS_AS(val.asInt(), Json::error); + CHECK_THROWS_AS(val.asUInt(), Json::error); + CHECK_THROWS_AS(val.asDouble(), Json::error); + CHECK(val.asBool() == false); // empty or not + + CHECK(val.isConvertibleTo(Json::nullValue)); + CHECK(!val.isConvertibleTo(Json::intValue)); + CHECK(!val.isConvertibleTo(Json::uintValue)); + CHECK(!val.isConvertibleTo(Json::realValue)); + CHECK(!val.isConvertibleTo(Json::stringValue)); + CHECK(!val.isConvertibleTo(Json::booleanValue)); + CHECK(val.isConvertibleTo(Json::arrayValue)); + CHECK(!val.isConvertibleTo(Json::objectValue)); + } + { + // object type + Json::Value val(Json::objectValue); + CHECK(val.isObject()); + // val.asCString should trigger an assertion failure + CHECK_THROWS_AS(val.asString(), Json::error); + CHECK_THROWS_AS(val.asInt(), Json::error); + CHECK_THROWS_AS(val.asUInt(), Json::error); + CHECK_THROWS_AS(val.asDouble(), Json::error); + CHECK(val.asBool() == false); // empty or not + + CHECK(val.isConvertibleTo(Json::nullValue)); + CHECK(!val.isConvertibleTo(Json::intValue)); + CHECK(!val.isConvertibleTo(Json::uintValue)); + CHECK(!val.isConvertibleTo(Json::realValue)); + CHECK(!val.isConvertibleTo(Json::stringValue)); + CHECK(!val.isConvertibleTo(Json::booleanValue)); + CHECK(!val.isConvertibleTo(Json::arrayValue)); + CHECK(val.isConvertibleTo(Json::objectValue)); + } +} + +TEST_CASE("access members") +{ + Json::Value val; + CHECK(val.type() == Json::nullValue); + CHECK(val.size() == 0); + CHECK(!val.isValidIndex(0)); + CHECK(!val.isMember("key")); + { + Json::Value const constVal = val; + CHECK(constVal[7u].type() == Json::nullValue); + CHECK(!constVal.isMember("key")); + CHECK(constVal["key"].type() == Json::nullValue); + CHECK(constVal.getMemberNames().empty()); + CHECK(constVal.get(1u, "default0") == "default0"); + CHECK(constVal.get(std::string("not"), "oh") == "oh"); + CHECK(constVal.get("missing", "default2") == "default2"); + } + + val = -7; + CHECK(val.type() == Json::intValue); + CHECK(val.size() == 0); + CHECK(!val.isValidIndex(0)); + CHECK(!val.isMember("key")); + + val = 42u; + CHECK(val.type() == Json::uintValue); + CHECK(val.size() == 0); + CHECK(!val.isValidIndex(0)); + CHECK(!val.isMember("key")); + + val = 3.14159; + CHECK(val.type() == Json::realValue); + CHECK(val.size() == 0); + CHECK(!val.isValidIndex(0)); + CHECK(!val.isMember("key")); + + val = true; + CHECK(val.type() == Json::booleanValue); + CHECK(val.size() == 0); + CHECK(!val.isValidIndex(0)); + CHECK(!val.isMember("key")); + + val = "string"; + CHECK(val.type() == Json::stringValue); + CHECK(val.size() == 0); + CHECK(!val.isValidIndex(0)); + CHECK(!val.isMember("key")); + + val = Json::Value(Json::objectValue); + CHECK(val.type() == Json::objectValue); + CHECK(val.size() == 0); + static Json::StaticString const staticThree("three"); + val[staticThree] = 3; + val["two"] = 2; + CHECK(val.size() == 2); + CHECK(val.isValidIndex(1)); + CHECK(!val.isValidIndex(2)); + CHECK(val[staticThree] == 3); + CHECK(val.isMember("two")); + CHECK(val.isMember(staticThree)); + CHECK(!val.isMember("key")); + { + Json::Value const constVal = val; + CHECK(constVal["two"] == 2); + CHECK(constVal["four"].type() == Json::nullValue); + CHECK(constVal[staticThree] == 3); + CHECK(constVal.isMember("two")); + CHECK(constVal.isMember(staticThree)); + CHECK(!constVal.isMember("key")); + CHECK(val.get(std::string("two"), "backup") == 2); + CHECK(val.get("missing", "default2") == "default2"); + } + + val = Json::Value(Json::arrayValue); + CHECK(val.type() == Json::arrayValue); + CHECK(val.size() == 0); + val[0u] = "zero"; + val[1u] = "one"; + CHECK(val.size() == 2); + CHECK(val.isValidIndex(1)); + CHECK(!val.isValidIndex(2)); + CHECK(val[20u].type() == Json::nullValue); + CHECK(!val.isMember("key")); + { + Json::Value const constVal = val; + CHECK(constVal[0u] == "zero"); + CHECK(constVal[2u].type() == Json::nullValue); + CHECK(!constVal.isMember("key")); + CHECK(val.get(1u, "default0") == "one"); + CHECK(val.get(3u, "default1") == "default1"); + } +} + +TEST_CASE("remove members") +{ + Json::Value val; + CHECK(val.removeMember(std::string("member")).type() == Json::nullValue); + + val = Json::Value(Json::objectValue); + static Json::StaticString const staticThree("three"); + val[staticThree] = 3; + val["two"] = 2; + CHECK(val.size() == 2); + + CHECK(val.removeMember(std::string("six")).type() == Json::nullValue); + CHECK(val.size() == 2); + + CHECK(val.removeMember(staticThree) == 3); + CHECK(val.size() == 1); + + CHECK(val.removeMember(staticThree).type() == Json::nullValue); + CHECK(val.size() == 1); + + CHECK(val.removeMember(std::string("two")) == 2); + CHECK(val.size() == 0); + + CHECK(val.removeMember(std::string("two")).type() == Json::nullValue); + CHECK(val.size() == 0); +} + +TEST_CASE("iterator") +{ + { + // Iterating an array. + Json::Value arr{Json::arrayValue}; + arr[0u] = "zero"; + arr[1u] = "one"; + arr[2u] = "two"; + arr[3u] = "three"; + + Json::ValueIterator const b{arr.begin()}; + Json::ValueIterator const e{arr.end()}; + + Json::ValueIterator i1 = b; + Json::ValueIterator i2 = e; + --i2; + + // key(), index(), and memberName() on an object iterator. + CHECK(b != e); + CHECK(!(b == e)); + CHECK(i1.key() == 0); + CHECK(i2.key() == 3); + CHECK(i1.index() == 0); + CHECK(i2.index() == 3); + CHECK(std::strcmp(i1.memberName(), "") == 0); + CHECK(std::strcmp(i2.memberName(), "") == 0); + + // Pre and post increment and decrement. + *i1++ = "0"; + CHECK(*i1 == "one"); + *i1 = "1"; + ++i1; + + *i2-- = "3"; + CHECK(*i2 == "two"); + CHECK(i1 == i2); + *i2 = "2"; + CHECK(*i1 == "2"); + } + { + // Iterating a const object. + Json::Value const obj{[]() { + Json::Value obj{Json::objectValue}; + obj["0"] = 0; + obj["1"] = 1; + obj["2"] = 2; + obj["3"] = 3; + return obj; + }()}; + + Json::ValueConstIterator i1{obj.begin()}; + Json::ValueConstIterator i2{obj.end()}; + --i2; + + // key(), index(), and memberName() on an object iterator. + CHECK(i1 != i2); + CHECK(!(i1 == i2)); + CHECK(i1.key() == "0"); + CHECK(i2.key() == "3"); + CHECK(i1.index() == -1); + CHECK(i2.index() == -1); + CHECK(std::strcmp(i1.memberName(), "0") == 0); + CHECK(std::strcmp(i2.memberName(), "3") == 0); + + // Pre and post increment and decrement. + CHECK(*i1++ == 0); + CHECK(*i1 == 1); + ++i1; + + CHECK(*i2-- == 3); + CHECK(*i2 == 2); + CHECK(i1 == i2); + CHECK(*i1 == 2); + } + { + // Iterating a non-const null object. + Json::Value nul{}; + CHECK(nul.begin() == nul.end()); + } + { + // Iterating a const Int. + Json::Value const i{-3}; + CHECK(i.begin() == i.end()); + } +} + +TEST_CASE("nest limits") +{ + Json::Reader r; + { + auto nest = [](std::uint32_t depth) -> std::string { + std::string s = "{"; + for (std::uint32_t i{1}; i <= depth; ++i) + s += "\"obj\":{"; + for (std::uint32_t i{1}; i <= depth; ++i) + s += "}"; + s += "}"; + return s; + }; + + { + // Within object nest limit + auto json{nest(std::min(10u, Json::Reader::nest_limit))}; + Json::Value j; + CHECK(r.parse(json, j)); + } + + { + // Exceed object nest limit + auto json{nest(Json::Reader::nest_limit + 1)}; + Json::Value j; + CHECK(!r.parse(json, j)); + } + } + + auto nest = [](std::uint32_t depth) -> std::string { + std::string s = "{"; + for (std::uint32_t i{1}; i <= depth; ++i) + s += "\"array\":[{"; + for (std::uint32_t i{1}; i <= depth; ++i) + s += "]}"; + s += "}"; + return s; + }; + { + // Exceed array nest limit + auto json{nest(Json::Reader::nest_limit + 1)}; + Json::Value j; + CHECK(!r.parse(json, j)); + } +} + +TEST_CASE("memory leak") +{ + // When run with the address sanitizer, this test confirms there is no + // memory leak with the scenarios below. + { + Json::Value a; + a[0u] = 1; + CHECK(a.type() == Json::arrayValue); + CHECK(a[0u].type() == Json::intValue); + a = std::move(a[0u]); + CHECK(a.type() == Json::intValue); + } + { + Json::Value b; + Json::Value temp; + temp["a"] = "Probably avoids the small string optimization"; + temp["b"] = "Also probably avoids the small string optimization"; + CHECK(temp.type() == Json::objectValue); + b.append(temp); + CHECK(temp.type() == Json::objectValue); + CHECK(b.size() == 1); + + b.append(std::move(temp)); + CHECK(b.size() == 2); + + // Note that the type() == nullValue check is implementation + // specific and not guaranteed to be valid in the future. + CHECK(temp.type() == Json::nullValue); + } +} + +TEST_SUITE_END(); + +} // namespace ripple diff --git a/src/tests/libxrpl/json/Writer.cpp b/src/tests/libxrpl/json/Writer.cpp new file mode 100644 index 0000000000..59251aaf60 --- /dev/null +++ b/src/tests/libxrpl/json/Writer.cpp @@ -0,0 +1,192 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include + +#include +#include + +using namespace ripple; +using namespace Json; + +TEST_SUITE_BEGIN("JsonWriter"); + +struct WriterFixture +{ + std::string output; + std::unique_ptr writer; + + WriterFixture() + { + writer = std::make_unique(stringOutput(output)); + } + + void + reset() + { + output.clear(); + writer = std::make_unique(stringOutput(output)); + } + + void + expectOutput(std::string const& expected) const + { + CHECK(output == expected); + } + + void + checkOutputAndReset(std::string const& expected) + { + expectOutput(expected); + reset(); + } +}; + +TEST_CASE_FIXTURE(WriterFixture, "trivial") +{ + CHECK(output.empty()); + checkOutputAndReset(""); +} + +TEST_CASE_FIXTURE(WriterFixture, "near trivial") +{ + CHECK(output.empty()); + writer->output(0); + checkOutputAndReset("0"); +} + +TEST_CASE_FIXTURE(WriterFixture, "primitives") +{ + writer->output(true); + checkOutputAndReset("true"); + + writer->output(false); + checkOutputAndReset("false"); + + writer->output(23); + checkOutputAndReset("23"); + + writer->output(23.0); + checkOutputAndReset("23.0"); + + writer->output(23.5); + checkOutputAndReset("23.5"); + + writer->output("a string"); + checkOutputAndReset(R"("a string")"); + + writer->output(nullptr); + checkOutputAndReset("null"); +} + +TEST_CASE_FIXTURE(WriterFixture, "empty") +{ + writer->startRoot(Writer::array); + writer->finish(); + checkOutputAndReset("[]"); + + writer->startRoot(Writer::object); + writer->finish(); + checkOutputAndReset("{}"); +} + +TEST_CASE_FIXTURE(WriterFixture, "escaping") +{ + writer->output("\\"); + checkOutputAndReset(R"("\\")"); + + writer->output("\""); + checkOutputAndReset(R"("\"")"); + + writer->output("\\\""); + checkOutputAndReset(R"("\\\"")"); + + writer->output("this contains a \\ in the middle of it."); + checkOutputAndReset(R"("this contains a \\ in the middle of it.")"); + + writer->output("\b\f\n\r\t"); + checkOutputAndReset(R"("\b\f\n\r\t")"); +} + +TEST_CASE_FIXTURE(WriterFixture, "array") +{ + writer->startRoot(Writer::array); + writer->append(12); + writer->finish(); + checkOutputAndReset("[12]"); +} + +TEST_CASE_FIXTURE(WriterFixture, "long array") +{ + writer->startRoot(Writer::array); + writer->append(12); + writer->append(true); + writer->append("hello"); + writer->finish(); + checkOutputAndReset(R"([12,true,"hello"])"); +} + +TEST_CASE_FIXTURE(WriterFixture, "embedded array simple") +{ + writer->startRoot(Writer::array); + writer->startAppend(Writer::array); + writer->finish(); + writer->finish(); + checkOutputAndReset("[[]]"); +} + +TEST_CASE_FIXTURE(WriterFixture, "object") +{ + writer->startRoot(Writer::object); + writer->set("hello", "world"); + writer->finish(); + checkOutputAndReset(R"({"hello":"world"})"); +} + +TEST_CASE_FIXTURE(WriterFixture, "complex object") +{ + writer->startRoot(Writer::object); + writer->set("hello", "world"); + writer->startSet(Writer::array, "array"); + writer->append(true); + writer->append(12); + writer->startAppend(Writer::array); + writer->startAppend(Writer::object); + writer->set("goodbye", "cruel world."); + writer->startSet(Writer::array, "subarray"); + writer->append(23.5); + writer->finishAll(); + checkOutputAndReset( + R"({"hello":"world","array":[true,12,[{"goodbye":"cruel world.","subarray":[23.5]}]]})"); +} + +TEST_CASE_FIXTURE(WriterFixture, "json value") +{ + Json::Value value(Json::objectValue); + value["foo"] = 23; + writer->startRoot(Writer::object); + writer->set("hello", value); + writer->finish(); + checkOutputAndReset(R"({"hello":{"foo":23}})"); +} + +TEST_SUITE_END(); diff --git a/src/tests/libxrpl/json/main.cpp b/src/tests/libxrpl/json/main.cpp new file mode 100644 index 0000000000..0a3f254ea8 --- /dev/null +++ b/src/tests/libxrpl/json/main.cpp @@ -0,0 +1,2 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include From 91fa6b229599b76b777fdd67ae5c33330da649e1 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Tue, 28 Oct 2025 14:26:25 +0000 Subject: [PATCH 07/28] ci: Only run .exe files during test phase on Windows (#5947) --- .github/workflows/reusable-test.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/reusable-test.yml b/.github/workflows/reusable-test.yml index cfd0fe6849..4b9c75334a 100644 --- a/.github/workflows/reusable-test.yml +++ b/.github/workflows/reusable-test.yml @@ -74,9 +74,11 @@ jobs: - name: Run the separate tests if: ${{ inputs.run_tests }} + env: + EXT: ${{ runner.os == 'Windows' && '.exe' || '' }} shell: bash run: | - for test_file in ./doctest/*; do + for test_file in ./doctest/*${EXT}; do echo "Executing $test_file" chmod +x "$test_file" if [[ "${{ runner.os }}" == "Windows" && "$test_file" == "./doctest/xrpl.test.net.exe" ]]; then From d9960d5ba0f65d60d5963e63a6848ba1150f11da Mon Sep 17 00:00:00 2001 From: Pratik Mankawde Date: Tue, 28 Oct 2025 15:10:38 +0000 Subject: [PATCH 08/28] refactor: Retire fix1543 amendment (#5926) Amendments activated for more than 2 years can be retired. This change retires the fix1543 amendment. Co-authored-by: Bart Thomee <11445373+bthomee@users.noreply.github.com> --- include/xrpl/protocol/detail/features.macro | 2 +- src/xrpld/app/tx/detail/Escrow.cpp | 21 --------------------- src/xrpld/app/tx/detail/Escrow.h | 9 --------- src/xrpld/app/tx/detail/PayChan.cpp | 19 ++----------------- src/xrpld/app/tx/detail/PayChan.h | 6 ------ 5 files changed, 3 insertions(+), 54 deletions(-) diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 6052814357..3e3309b444 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -111,7 +111,6 @@ XRPL_FEATURE(MultiSignReserve, Supported::yes, VoteBehavior::DefaultYe XRPL_FIX (1578, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(DepositPreauth, Supported::yes, VoteBehavior::DefaultYes) XRPL_FIX (1623, Supported::yes, VoteBehavior::DefaultYes) -XRPL_FIX (1543, Supported::yes, VoteBehavior::DefaultYes) XRPL_FIX (1571, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(Checks, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(DepositAuth, Supported::yes, VoteBehavior::DefaultYes) @@ -155,3 +154,4 @@ XRPL_RETIRE(fix1528) XRPL_RETIRE(FlowCross) XRPL_RETIRE(fix1513) XRPL_RETIRE(fix1515) +XRPL_RETIRE(fix1543) diff --git a/src/xrpld/app/tx/detail/Escrow.cpp b/src/xrpld/app/tx/detail/Escrow.cpp index 42b7c8e458..a5c44b1e2e 100644 --- a/src/xrpld/app/tx/detail/Escrow.cpp +++ b/src/xrpld/app/tx/detail/Escrow.cpp @@ -118,13 +118,6 @@ escrowCreatePreflightHelper(PreflightContext const& ctx) return tesSUCCESS; } -std::uint32_t -EscrowCreate::getFlagsMask(PreflightContext const& ctx) -{ - // 0 means "Allow any flags" - return ctx.rules.enabled(fix1543) ? tfUniversalMask : 0; -} - NotTEC EscrowCreate::preflight(PreflightContext const& ctx) { @@ -639,13 +632,6 @@ EscrowFinish::checkExtraFeatures(PreflightContext const& ctx) ctx.rules.enabled(featureCredentials); } -std::uint32_t -EscrowFinish::getFlagsMask(PreflightContext const& ctx) -{ - // 0 means "Allow any flags" - return ctx.rules.enabled(fix1543) ? tfUniversalMask : 0; -} - NotTEC EscrowFinish::preflight(PreflightContext const& ctx) { @@ -1225,13 +1211,6 @@ EscrowFinish::doApply() //------------------------------------------------------------------------------ -std::uint32_t -EscrowCancel::getFlagsMask(PreflightContext const& ctx) -{ - // 0 means "Allow any flags" - return ctx.rules.enabled(fix1543) ? tfUniversalMask : 0; -} - NotTEC EscrowCancel::preflight(PreflightContext const& ctx) { diff --git a/src/xrpld/app/tx/detail/Escrow.h b/src/xrpld/app/tx/detail/Escrow.h index 8956be2939..55816734eb 100644 --- a/src/xrpld/app/tx/detail/Escrow.h +++ b/src/xrpld/app/tx/detail/Escrow.h @@ -36,9 +36,6 @@ public: static TxConsequences makeTxConsequences(PreflightContext const& ctx); - static std::uint32_t - getFlagsMask(PreflightContext const& ctx); - static NotTEC preflight(PreflightContext const& ctx); @@ -63,9 +60,6 @@ public: static bool checkExtraFeatures(PreflightContext const& ctx); - static std::uint32_t - getFlagsMask(PreflightContext const& ctx); - static NotTEC preflight(PreflightContext const& ctx); @@ -93,9 +87,6 @@ public: { } - static std::uint32_t - getFlagsMask(PreflightContext const& ctx); - static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/PayChan.cpp b/src/xrpld/app/tx/detail/PayChan.cpp index b495e00b3f..b2353e3078 100644 --- a/src/xrpld/app/tx/detail/PayChan.cpp +++ b/src/xrpld/app/tx/detail/PayChan.cpp @@ -175,13 +175,6 @@ PayChanCreate::makeTxConsequences(PreflightContext const& ctx) return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()}; } -std::uint32_t -PayChanCreate::getFlagsMask(PreflightContext const& ctx) -{ - // 0 means "Allow any flags" - return ctx.rules.enabled(fix1543) ? tfUniversalMask : 0; -} - NotTEC PayChanCreate::preflight(PreflightContext const& ctx) { @@ -335,13 +328,6 @@ PayChanFund::makeTxConsequences(PreflightContext const& ctx) return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()}; } -std::uint32_t -PayChanFund::getFlagsMask(PreflightContext const& ctx) -{ - // 0 means "Allow any flags" - return ctx.rules.enabled(fix1543) ? tfUniversalMask : 0; -} - NotTEC PayChanFund::preflight(PreflightContext const& ctx) { @@ -434,10 +420,9 @@ PayChanClaim::checkExtraFeatures(PreflightContext const& ctx) } std::uint32_t -PayChanClaim::getFlagsMask(PreflightContext const& ctx) +PayChanClaim::getFlagsMask(PreflightContext const&) { - // 0 means "Allow any flags" - return ctx.rules.enabled(fix1543) ? tfPayChanClaimMask : 0; + return tfPayChanClaimMask; } NotTEC diff --git a/src/xrpld/app/tx/detail/PayChan.h b/src/xrpld/app/tx/detail/PayChan.h index b25a4529be..0042f5c7c9 100644 --- a/src/xrpld/app/tx/detail/PayChan.h +++ b/src/xrpld/app/tx/detail/PayChan.h @@ -36,9 +36,6 @@ public: static TxConsequences makeTxConsequences(PreflightContext const& ctx); - static std::uint32_t - getFlagsMask(PreflightContext const& ctx); - static NotTEC preflight(PreflightContext const& ctx); @@ -65,9 +62,6 @@ public: static TxConsequences makeTxConsequences(PreflightContext const& ctx); - static std::uint32_t - getFlagsMask(PreflightContext const& ctx); - static NotTEC preflight(PreflightContext const& ctx); From 7d5ed0cd8d9e82e0bd741a87836878e5f7d678fd Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Oct 2025 17:42:43 +0000 Subject: [PATCH 09/28] fix: account_tx limit parameter validation for malformed values (#5891) This change fixes the `account_tx` RPC method to properly validate malformed limit parameter values. Previously, invalid values like `0`, `1.2`, `"10"`, `true`, `false`, `-1`, `[]`, `{}`, etc. were either accepted without errors or caused internal errors. Now all malformed values correctly return the `invalidParams` error. Co-authored-by: Bart Thomee <11445373+bthomee@users.noreply.github.com> --- src/test/rpc/AccountOffers_test.cpp | 19 +-- src/test/rpc/AccountTx_test.cpp | 145 ++++++++++++++++++--- src/test/rpc/Book_test.cpp | 19 ++- src/xrpld/rpc/detail/RPCHelpers.cpp | 22 ++-- src/xrpld/rpc/detail/Tuning.h | 3 + src/xrpld/rpc/handlers/AccountChannels.cpp | 3 - src/xrpld/rpc/handlers/AccountLines.cpp | 3 - src/xrpld/rpc/handlers/AccountOffers.cpp | 3 - src/xrpld/rpc/handlers/AccountTx.cpp | 7 +- 9 files changed, 166 insertions(+), 58 deletions(-) diff --git a/src/test/rpc/AccountOffers_test.cpp b/src/test/rpc/AccountOffers_test.cpp index 2aa85ac9b9..59a6942ed2 100644 --- a/src/test/rpc/AccountOffers_test.cpp +++ b/src/test/rpc/AccountOffers_test.cpp @@ -190,12 +190,6 @@ public: } { - // now make a limit (= 0) query for the same data - // since we operate on the admin port, the limit - // value of 0 is not adjusted into tuned ranges for admin requests - // so we literally get 0 elements in that case. For non-admin - // requests, we get limit defaults applied thus all our results - // come back (we are below the min results limit) Json::Value jvParams; jvParams[jss::account] = bob.human(); jvParams[jss::limit] = 0u; @@ -203,18 +197,7 @@ public: "json", "account_offers", jvParams.toStyledString())[jss::result]; - auto const& jro = jrr[jss::offers]; - if (asAdmin) - { - // limit == 0 is invalid - BEAST_EXPECT(jrr.isMember(jss::error_message)); - } - else - { - // Call should enforce min limit of 10 - BEAST_EXPECT(checkArraySize(jro, 3u)); - BEAST_EXPECT(!jrr.isMember(jss::marker)); - } + BEAST_EXPECT(jrr.isMember(jss::error_message)); } } diff --git a/src/test/rpc/AccountTx_test.cpp b/src/test/rpc/AccountTx_test.cpp index 82809b5c5b..cbe7cc8f4a 100644 --- a/src/test/rpc/AccountTx_test.cpp +++ b/src/test/rpc/AccountTx_test.cpp @@ -193,26 +193,26 @@ class AccountTx_test : public beast::unit_test::suite j[jss::result][jss::error] == RPC::get_error_info(code).token; }; - Json::Value jParms; - jParms[jss::api_version] = apiVersion; + Json::Value jParams; + jParams[jss::api_version] = apiVersion; BEAST_EXPECT(isErr( - env.rpc("json", "account_tx", to_string(jParms)), + env.rpc("json", "account_tx", to_string(jParams)), rpcINVALID_PARAMS)); - jParms[jss::account] = "0xDEADBEEF"; + jParams[jss::account] = "0xDEADBEEF"; BEAST_EXPECT(isErr( - env.rpc("json", "account_tx", to_string(jParms)), + env.rpc("json", "account_tx", to_string(jParams)), rpcACT_MALFORMED)); - jParms[jss::account] = A1.human(); + jParams[jss::account] = A1.human(); BEAST_EXPECT(hasTxs( - env.rpc(apiVersion, "json", "account_tx", to_string(jParms)))); + env.rpc(apiVersion, "json", "account_tx", to_string(jParams)))); // Ledger min/max index { - Json::Value p{jParms}; + Json::Value p{jParams}; p[jss::ledger_index_min] = -1; p[jss::ledger_index_max] = -1; BEAST_EXPECT(hasTxs( @@ -247,7 +247,7 @@ class AccountTx_test : public beast::unit_test::suite } // Ledger index min only { - Json::Value p{jParms}; + Json::Value p{jParams}; p[jss::ledger_index_min] = -1; BEAST_EXPECT(hasTxs( env.rpc(apiVersion, "json", "account_tx", to_string(p)))); @@ -270,7 +270,7 @@ class AccountTx_test : public beast::unit_test::suite // Ledger index max only { - Json::Value p{jParms}; + Json::Value p{jParams}; p[jss::ledger_index_max] = -1; BEAST_EXPECT(hasTxs( env.rpc(apiVersion, "json", "account_tx", to_string(p)))); @@ -298,7 +298,7 @@ class AccountTx_test : public beast::unit_test::suite // Ledger Sequence { - Json::Value p{jParms}; + Json::Value p{jParams}; p[jss::ledger_index] = env.closed()->info().seq; BEAST_EXPECT(hasTxs( @@ -319,7 +319,7 @@ class AccountTx_test : public beast::unit_test::suite // Ledger Hash { - Json::Value p{jParms}; + Json::Value p{jParams}; p[jss::ledger_hash] = to_string(env.closed()->info().hash); BEAST_EXPECT(hasTxs( @@ -332,9 +332,9 @@ class AccountTx_test : public beast::unit_test::suite // Ledger index max/min/index all specified // ERRORS out with invalid Parenthesis { - jParms[jss::account] = "0xDEADBEEF"; - jParms[jss::account] = A1.human(); - Json::Value p{jParms}; + jParams[jss::account] = "0xDEADBEEF"; + jParams[jss::account] = A1.human(); + Json::Value p{jParams}; p[jss::ledger_index_max] = -1; p[jss::ledger_index_min] = -1; @@ -351,7 +351,7 @@ class AccountTx_test : public beast::unit_test::suite // Ledger index max only { - Json::Value p{jParms}; + Json::Value p{jParams}; p[jss::ledger_index_max] = env.current()->info().seq; if (apiVersion < 2u) BEAST_EXPECT(hasTxs( @@ -382,7 +382,7 @@ class AccountTx_test : public beast::unit_test::suite } // test binary and forward for bool/non bool values { - Json::Value p{jParms}; + Json::Value p{jParams}; p[jss::binary] = "asdf"; if (apiVersion < 2u) { @@ -410,6 +410,117 @@ class AccountTx_test : public beast::unit_test::suite result = env.rpc("json", "account_tx", to_string(p)); BEAST_EXPECT(result[jss::result][jss::status] == "success"); } + // test limit with malformed values + { + Json::Value p{jParams}; + + // Test case: limit = 0 should fail (below minimum) + p[jss::limit] = 0; + BEAST_EXPECT(isErr( + env.rpc("json", "account_tx", to_string(p)), + rpcINVALID_PARAMS)); + + // Test case: limit = 1.2 should fail (not an integer) + p[jss::limit] = 1.2; + BEAST_EXPECT( + env.rpc( + "json", + "account_tx", + to_string(p))[jss::result][jss::error_message] == + RPC::expected_field_message(jss::limit, "unsigned integer")); + + // Test case: limit = "10" should fail (string instead of integer) + p[jss::limit] = "10"; + BEAST_EXPECT( + env.rpc( + "json", + "account_tx", + to_string(p))[jss::result][jss::error_message] == + RPC::expected_field_message(jss::limit, "unsigned integer")); + + // Test case: limit = true should fail (boolean instead of integer) + p[jss::limit] = true; + BEAST_EXPECT( + env.rpc( + "json", + "account_tx", + to_string(p))[jss::result][jss::error_message] == + RPC::expected_field_message(jss::limit, "unsigned integer")); + + // Test case: limit = false should fail (boolean instead of integer) + p[jss::limit] = false; + BEAST_EXPECT( + env.rpc( + "json", + "account_tx", + to_string(p))[jss::result][jss::error_message] == + RPC::expected_field_message(jss::limit, "unsigned integer")); + + // Test case: limit = -1 should fail (negative number) + p[jss::limit] = -1; + BEAST_EXPECT( + env.rpc( + "json", + "account_tx", + to_string(p))[jss::result][jss::error_message] == + RPC::expected_field_message(jss::limit, "unsigned integer")); + + // Test case: limit = [] should fail (array instead of integer) + p[jss::limit] = Json::Value(Json::arrayValue); + BEAST_EXPECT( + env.rpc( + "json", + "account_tx", + to_string(p))[jss::result][jss::error_message] == + RPC::expected_field_message(jss::limit, "unsigned integer")); + + // Test case: limit = {} should fail (object instead of integer) + p[jss::limit] = Json::Value(Json::objectValue); + BEAST_EXPECT( + env.rpc( + "json", + "account_tx", + to_string(p))[jss::result][jss::error_message] == + RPC::expected_field_message(jss::limit, "unsigned integer")); + + // Test case: limit = "malformed" should fail (malformed string) + p[jss::limit] = "malformed"; + BEAST_EXPECT( + env.rpc( + "json", + "account_tx", + to_string(p))[jss::result][jss::error_message] == + RPC::expected_field_message(jss::limit, "unsigned integer")); + + // Test case: limit = ["limit"] should fail (array with string) + p[jss::limit] = Json::Value(Json::arrayValue); + p[jss::limit].append("limit"); + BEAST_EXPECT( + env.rpc( + "json", + "account_tx", + to_string(p))[jss::result][jss::error_message] == + RPC::expected_field_message(jss::limit, "unsigned integer")); + + // Test case: limit = {"limit": 10} should fail (object with + // property) + p[jss::limit] = Json::Value(Json::objectValue); + p[jss::limit][jss::limit] = 10; + BEAST_EXPECT( + env.rpc( + "json", + "account_tx", + to_string(p))[jss::result][jss::error_message] == + RPC::expected_field_message(jss::limit, "unsigned integer")); + + // Test case: limit = 10 should succeed (valid integer) + p[jss::limit] = 10; + BEAST_EXPECT( + env.rpc( + "json", + "account_tx", + to_string(p))[jss::result][jss::status] == "success"); + } } void diff --git a/src/test/rpc/Book_test.cpp b/src/test/rpc/Book_test.cpp index 7177ab847c..0d5366d3ef 100644 --- a/src/test/rpc/Book_test.cpp +++ b/src/test/rpc/Book_test.cpp @@ -1633,6 +1633,20 @@ public: "Invalid field 'limit', not unsigned integer."); } + { + Json::Value jvParams; + jvParams[jss::ledger_index] = "validated"; + jvParams[jss::taker] = env.master.human(); + jvParams[jss::limit] = 0; // must be > 0 + jvParams[jss::taker_pays][jss::currency] = "XRP"; + jvParams[jss::taker_gets][jss::currency] = "USD"; + jvParams[jss::taker_gets][jss::issuer] = gw.human(); + auto const jrr = env.rpc( + "json", "book_offers", to_string(jvParams))[jss::result]; + BEAST_EXPECT(jrr[jss::error] == "invalidParams"); + BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'limit'."); + } + { Json::Value jvParams; jvParams[jss::ledger_index] = "validated"; @@ -1710,11 +1724,6 @@ public: BEAST_EXPECT(jrr[jss::offers].size() == (asAdmin ? 1u : 0u)); // NOTE - a marker field is not returned for this method - jvParams[jss::limit] = 0u; - jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result]; - BEAST_EXPECT(jrr[jss::offers].isArray()); - BEAST_EXPECT(jrr[jss::offers].size() == 0u); - jvParams[jss::limit] = RPC::Tuning::bookOffers.rmax + 1; jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result]; BEAST_EXPECT(jrr[jss::offers].isArray()); diff --git a/src/xrpld/rpc/detail/RPCHelpers.cpp b/src/xrpld/rpc/detail/RPCHelpers.cpp index edb91611be..9c49751bcd 100644 --- a/src/xrpld/rpc/detail/RPCHelpers.cpp +++ b/src/xrpld/rpc/detail/RPCHelpers.cpp @@ -704,15 +704,21 @@ readLimitField( JsonContext const& context) { limit = range.rdefault; - if (auto const& jvLimit = context.params[jss::limit]) - { - if (!(jvLimit.isUInt() || (jvLimit.isInt() && jvLimit.asInt() >= 0))) - return RPC::expected_field_error(jss::limit, "unsigned integer"); + if (!context.params.isMember(jss::limit) || + context.params[jss::limit].isNull()) + return std::nullopt; + + auto const& jvLimit = context.params[jss::limit]; + if (!(jvLimit.isUInt() || (jvLimit.isInt() && jvLimit.asInt() >= 0))) + return RPC::expected_field_error(jss::limit, "unsigned integer"); + + limit = jvLimit.asUInt(); + if (limit == 0) + return RPC::invalid_field_error(jss::limit); + + if (!isUnlimited(context.role)) + limit = std::max(range.rmin, std::min(range.rmax, limit)); - limit = jvLimit.asUInt(); - if (!isUnlimited(context.role)) - limit = std::max(range.rmin, std::min(range.rmax, limit)); - } return std::nullopt; } diff --git a/src/xrpld/rpc/detail/Tuning.h b/src/xrpld/rpc/detail/Tuning.h index 4f4a8be1bf..f3218a3fb1 100644 --- a/src/xrpld/rpc/detail/Tuning.h +++ b/src/xrpld/rpc/detail/Tuning.h @@ -45,6 +45,9 @@ static LimitRange constexpr accountObjects = {10, 200, 400}; /** Limits for the account_offers command. */ static LimitRange constexpr accountOffers = {10, 200, 400}; +/** Limits for the account_tx command. */ +static LimitRange constexpr accountTx = {10, 200, 400}; + /** Limits for the book_offers command. */ static LimitRange constexpr bookOffers = {0, 60, 100}; diff --git a/src/xrpld/rpc/handlers/AccountChannels.cpp b/src/xrpld/rpc/handlers/AccountChannels.cpp index 17e46f052f..1a21f67860 100644 --- a/src/xrpld/rpc/handlers/AccountChannels.cpp +++ b/src/xrpld/rpc/handlers/AccountChannels.cpp @@ -103,9 +103,6 @@ doAccountChannels(RPC::JsonContext& context) if (auto err = readLimitField(limit, RPC::Tuning::accountChannels, context)) return *err; - if (limit == 0u) - return rpcError(rpcINVALID_PARAMS); - Json::Value jsonChannels{Json::arrayValue}; struct VisitData { diff --git a/src/xrpld/rpc/handlers/AccountLines.cpp b/src/xrpld/rpc/handlers/AccountLines.cpp index 146a9527a9..9fb076445b 100644 --- a/src/xrpld/rpc/handlers/AccountLines.cpp +++ b/src/xrpld/rpc/handlers/AccountLines.cpp @@ -120,9 +120,6 @@ doAccountLines(RPC::JsonContext& context) if (auto err = readLimitField(limit, RPC::Tuning::accountLines, context)) return *err; - if (limit == 0) - return rpcError(rpcINVALID_PARAMS); - // this flag allows the requester to ask incoming trustlines in default // state be omitted bool ignoreDefault = params.isMember(jss::ignore_default) && diff --git a/src/xrpld/rpc/handlers/AccountOffers.cpp b/src/xrpld/rpc/handlers/AccountOffers.cpp index 1f2b76efe4..737f39cc13 100644 --- a/src/xrpld/rpc/handlers/AccountOffers.cpp +++ b/src/xrpld/rpc/handlers/AccountOffers.cpp @@ -86,9 +86,6 @@ doAccountOffers(RPC::JsonContext& context) if (auto err = readLimitField(limit, RPC::Tuning::accountOffers, context)) return *err; - if (limit == 0) - return RPC::invalid_field_error(jss::limit); - Json::Value& jsonOffers(result[jss::offers] = Json::arrayValue); std::vector> offers; uint256 startAfter = beast::zero; diff --git a/src/xrpld/rpc/handlers/AccountTx.cpp b/src/xrpld/rpc/handlers/AccountTx.cpp index e053c2adc0..64247c732e 100644 --- a/src/xrpld/rpc/handlers/AccountTx.cpp +++ b/src/xrpld/rpc/handlers/AccountTx.cpp @@ -26,6 +26,8 @@ #include #include #include +#include +#include #include #include @@ -429,7 +431,10 @@ doAccountTxJson(RPC::JsonContext& context) return RPC::invalid_field_error(jss::forward); } - args.limit = params.isMember(jss::limit) ? params[jss::limit].asUInt() : 0; + if (auto const err = + RPC::readLimitField(args.limit, RPC::Tuning::accountTx, context)) + return *err; + args.binary = params.isMember(jss::binary) && params[jss::binary].asBool(); args.forward = params.isMember(jss::forward) && params[jss::forward].asBool(); From 21f3c12d85a34dcc077287b0b42db5a44dac3814 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Tue, 28 Oct 2025 18:12:11 +0000 Subject: [PATCH 10/28] fix: invariant error in fee-sized `VaultWithdraw` (#5876) This changes fixes an invariant error where the amount withdrawn is equal to the transaction fee. Co-authored-by: Bart Thomee <11445373+bthomee@users.noreply.github.com> --- src/test/app/Invariants_test.cpp | 153 +++++++++++----- src/test/app/Vault_test.cpp | 194 +++++++++++++++++++++ src/xrpld/app/tx/detail/InvariantCheck.cpp | 79 ++++----- 3 files changed, 338 insertions(+), 88 deletions(-) diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index 8781eee72b..925776a1b4 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -1751,7 +1751,7 @@ class Invariants_test : public beast::unit_test::suite AccountID account; int amount; }; - struct Adjustements + struct Adjustments { std::optional assetsTotal = {}; std::optional assetsAvailable = {}; @@ -1764,7 +1764,7 @@ class Invariants_test : public beast::unit_test::suite }; auto constexpr adjust = [&](ApplyView& ac, ripple::Keylet keylet, - Adjustements args) { + Adjustments args) { auto sleVault = ac.peek(keylet); if (!sleVault) return false; @@ -1790,9 +1790,11 @@ class Invariants_test : public beast::unit_test::suite ac.update(sleVault); if (args.sharesTotal) + { (*sleShares)[sfOutstandingAmount] = *(*sleShares)[sfOutstandingAmount] + *args.sharesTotal; - ac.update(sleShares); + ac.update(sleShares); + } auto const assets = *(*sleVault)[sfAsset]; auto const pseudoId = *(*sleVault)[sfAccount]; @@ -1863,17 +1865,17 @@ class Invariants_test : public beast::unit_test::suite }; constexpr auto args = - [](AccountID id, int adjustement, auto fn) -> Adjustements { - Adjustements sample = { - .assetsTotal = adjustement, - .assetsAvailable = adjustement, + [](AccountID id, int adjustment, auto fn) -> Adjustments { + Adjustments sample = { + .assetsTotal = adjustment, + .assetsAvailable = adjustment, .lossUnrealized = 0, - .sharesTotal = adjustement, - .vaultAssets = adjustement, + .sharesTotal = adjustment, + .vaultAssets = adjustment, .accountAssets = // - AccountAmount{id, -adjustement}, + AccountAmount{id, -adjustment}, .accountShares = // - AccountAmount{id, adjustement}}; + AccountAmount{id, adjustment}}; fn(sample); return sample; }; @@ -2285,7 +2287,7 @@ class Invariants_test : public beast::unit_test::suite return adjust( ac.view(), keylet, - args(A2.id(), 0, [&](Adjustements& sample) { + args(A2.id(), 0, [&](Adjustments& sample) { sample.assetsAvailable = (DROPS_PER_XRP * -100).value(); sample.assetsTotal = (DROPS_PER_XRP * -200).value(); sample.sharesTotal = -1; @@ -2354,7 +2356,7 @@ class Invariants_test : public beast::unit_test::suite return adjust( ac.view(), keylet, - args(A2.id(), 0, [&](Adjustements& sample) { + args(A2.id(), 0, [&](Adjustments& sample) { sample.lossUnrealized = 13; sample.assetsTotal = 20; })); @@ -2374,7 +2376,7 @@ class Invariants_test : public beast::unit_test::suite return adjust( ac.view(), keylet, - args(A2.id(), 100, [&](Adjustements& sample) { + args(A2.id(), 100, [&](Adjustments& sample) { sample.lossUnrealized = 13; })); }, @@ -2395,7 +2397,7 @@ class Invariants_test : public beast::unit_test::suite return adjust( ac.view(), keylet, - args(A2.id(), 0, [&](Adjustements& sample) { + args(A2.id(), 0, [&](Adjustments& sample) { sample.assetsMaximum = 1; })); }, @@ -2412,7 +2414,7 @@ class Invariants_test : public beast::unit_test::suite return adjust( ac.view(), keylet, - args(A2.id(), 0, [&](Adjustements& sample) { + args(A2.id(), 0, [&](Adjustments& sample) { sample.assetsMaximum = -1; })); }, @@ -2461,7 +2463,7 @@ class Invariants_test : public beast::unit_test::suite ac.view().update(sleShares); return adjust( - ac.view(), keylet, args(A2.id(), 10, [](Adjustements&) {})); + ac.view(), keylet, args(A2.id(), 10, [](Adjustments&) {})); }, XRPAmount{}, STTx{ttVAULT_DEPOSIT, [](STObject&) {}}, @@ -2474,7 +2476,7 @@ class Invariants_test : public beast::unit_test::suite [&](Account const& A1, Account const& A2, ApplyContext& ac) { auto const keylet = keylet::vault(A1.id(), ac.view().seq()); adjust( - ac.view(), keylet, args(A2.id(), 10, [](Adjustements&) {})); + ac.view(), keylet, args(A2.id(), 10, [](Adjustments&) {})); auto sleVault = ac.view().peek(keylet); if (!sleVault) @@ -2850,7 +2852,9 @@ class Invariants_test : public beast::unit_test::suite return adjust( ac.view(), keylet, - args(A2.id(), 0, [&](Adjustements& sample) {})); + args(A2.id(), 0, [](Adjustments& sample) { + sample.vaultAssets.reset(); + })); }, XRPAmount{}, STTx{ttVAULT_DEPOSIT, [](STObject&) {}}, @@ -2864,7 +2868,7 @@ class Invariants_test : public beast::unit_test::suite return adjust( ac.view(), keylet, - args(A2.id(), 200, [&](Adjustements& sample) { + args(A2.id(), 200, [&](Adjustments& sample) { sample.assetsMaximum = 1; })); }, @@ -2898,7 +2902,7 @@ class Invariants_test : public beast::unit_test::suite return adjust( ac.view(), keylet, - args(A3.id(), -10, [&](Adjustements& sample) { + args(A3.id(), -10, [&](Adjustments& sample) { sample.accountAssets->amount = -100; })); }, @@ -2931,7 +2935,7 @@ class Invariants_test : public beast::unit_test::suite return adjust( ac.view(), keylet, - args(A2.id(), 10, [&](Adjustements& sample) { + args(A2.id(), 10, [&](Adjustments& sample) { sample.vaultAssets = -20; sample.accountAssets->amount = 10; })); @@ -2959,7 +2963,7 @@ class Invariants_test : public beast::unit_test::suite return adjust( ac.view(), keylet, - args(A2.id(), 10, [&](Adjustements& sample) { + args(A2.id(), 10, [&](Adjustments& sample) { sample.accountAssets->amount = 0; })); }, @@ -2978,8 +2982,8 @@ class Invariants_test : public beast::unit_test::suite return adjust( ac.view(), keylet, - args(A2.id(), 10, [&](Adjustements& sample) { - sample.accountShares->amount = 0; + args(A2.id(), 10, [&](Adjustments& sample) { + sample.accountShares.reset(); })); }, XRPAmount{}, @@ -2994,10 +2998,11 @@ class Invariants_test : public beast::unit_test::suite {"deposit must change vault shares"}, [&](Account const& A1, Account const& A2, ApplyContext& ac) { auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + return adjust( ac.view(), keylet, - args(A2.id(), 10, [&](Adjustements& sample) { + args(A2.id(), 10, [](Adjustments& sample) { sample.sharesTotal = 0; })); }, @@ -3019,7 +3024,7 @@ class Invariants_test : public beast::unit_test::suite return adjust( ac.view(), keylet, - args(A2.id(), 10, [&](Adjustements& sample) { + args(A2.id(), 10, [&](Adjustments& sample) { sample.accountShares->amount = -5; sample.sharesTotal = -10; })); @@ -3032,6 +3037,33 @@ class Invariants_test : public beast::unit_test::suite precloseXrp, TxAccount::A2); + doInvariantCheck( + {"deposit and assets outstanding must add up"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto sleA3 = ac.view().peek(keylet::account(A3.id())); + (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] - 2000; + ac.view().update(sleA3); + + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + return adjust( + ac.view(), + keylet, + args(A2.id(), 10, [&](Adjustments& sample) { + sample.assetsTotal = 11; + })); + }, + XRPAmount{2000}, + STTx{ + ttVAULT_DEPOSIT, + [&](STObject& tx) { + tx[sfAmount] = XRPAmount(10); + tx[sfDelegate] = A3.id(); + tx[sfFee] = XRPAmount(2000); + }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + doInvariantCheck( {"deposit and assets outstanding must add up", "deposit and assets available must add up"}, @@ -3040,7 +3072,7 @@ class Invariants_test : public beast::unit_test::suite return adjust( ac.view(), keylet, - args(A2.id(), 10, [&](Adjustements& sample) { + args(A2.id(), 10, [&](Adjustments& sample) { sample.assetsTotal = 7; sample.assetsAvailable = 7; })); @@ -3061,7 +3093,9 @@ class Invariants_test : public beast::unit_test::suite return adjust( ac.view(), keylet, - args(A2.id(), 0, [&](Adjustements& sample) {})); + args(A2.id(), 0, [](Adjustments& sample) { + sample.vaultAssets.reset(); + })); }, XRPAmount{}, STTx{ttVAULT_WITHDRAW, [](STObject&) {}}, @@ -3087,7 +3121,7 @@ class Invariants_test : public beast::unit_test::suite return adjust( ac.view(), keylet, - args(A3.id(), -10, [&](Adjustements& sample) { + args(A3.id(), -10, [&](Adjustments& sample) { sample.accountAssets->amount = -100; })); }, @@ -3123,7 +3157,7 @@ class Invariants_test : public beast::unit_test::suite return adjust( ac.view(), keylet, - args(A2.id(), -10, [&](Adjustements& sample) { + args(A2.id(), -10, [&](Adjustments& sample) { sample.vaultAssets = 10; sample.accountAssets->amount = -20; })); @@ -3141,7 +3175,7 @@ class Invariants_test : public beast::unit_test::suite if (!adjust( ac.view(), keylet, - args(A2.id(), -10, [&](Adjustements& sample) { + args(A2.id(), -10, [&](Adjustments& sample) { *sample.vaultAssets -= 5; }))) return false; @@ -3167,8 +3201,8 @@ class Invariants_test : public beast::unit_test::suite return adjust( ac.view(), keylet, - args(A2.id(), -10, [&](Adjustements& sample) { - sample.accountShares->amount = 0; + args(A2.id(), -10, [&](Adjustments& sample) { + sample.accountShares.reset(); })); }, XRPAmount{}, @@ -3184,7 +3218,7 @@ class Invariants_test : public beast::unit_test::suite return adjust( ac.view(), keylet, - args(A2.id(), -10, [&](Adjustements& sample) { + args(A2.id(), -10, [](Adjustments& sample) { sample.sharesTotal = 0; })); }, @@ -3203,7 +3237,7 @@ class Invariants_test : public beast::unit_test::suite return adjust( ac.view(), keylet, - args(A2.id(), -10, [&](Adjustements& sample) { + args(A2.id(), -10, [&](Adjustments& sample) { sample.accountShares->amount = 5; sample.sharesTotal = 10; })); @@ -3222,7 +3256,7 @@ class Invariants_test : public beast::unit_test::suite return adjust( ac.view(), keylet, - args(A2.id(), -10, [&](Adjustements& sample) { + args(A2.id(), -10, [&](Adjustments& sample) { sample.assetsTotal = -15; sample.assetsAvailable = -15; })); @@ -3233,6 +3267,33 @@ class Invariants_test : public beast::unit_test::suite precloseXrp, TxAccount::A2); + doInvariantCheck( + {"withdrawal and assets outstanding must add up"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto sleA3 = ac.view().peek(keylet::account(A3.id())); + (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] - 2000; + ac.view().update(sleA3); + + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + return adjust( + ac.view(), + keylet, + args(A2.id(), -10, [&](Adjustments& sample) { + sample.assetsTotal = -7; + })); + }, + XRPAmount{2000}, + STTx{ + ttVAULT_WITHDRAW, + [&](STObject& tx) { + tx[sfAmount] = XRPAmount(10); + tx[sfDelegate] = A3.id(); + tx[sfFee] = XRPAmount(2000); + }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + auto const precloseMpt = [&](Account const& A1, Account const& A2, Env& env) -> bool { env.fund(XRP(1000), A3, A4); @@ -3292,7 +3353,7 @@ class Invariants_test : public beast::unit_test::suite return adjust( ac.view(), keylet, - args(A2.id(), -10, [&](Adjustements& sample) { + args(A2.id(), -10, [&](Adjustments& sample) { sample.accountShares->amount = 5; })); }, @@ -3312,8 +3373,8 @@ class Invariants_test : public beast::unit_test::suite return adjust( ac.view(), keylet, - args(A2.id(), -1, [&](Adjustements& sample) { - sample.vaultAssets = 0; + args(A2.id(), -1, [&](Adjustments& sample) { + sample.vaultAssets.reset(); })); }, XRPAmount{}, @@ -3331,7 +3392,7 @@ class Invariants_test : public beast::unit_test::suite return adjust( ac.view(), keylet, - args(A2.id(), 0, [&](Adjustements& sample) {})); + args(A2.id(), 0, [&](Adjustments& sample) {})); }, XRPAmount{}, STTx{ttVAULT_CLAWBACK, [](STObject&) {}}, @@ -3346,7 +3407,7 @@ class Invariants_test : public beast::unit_test::suite return adjust( ac.view(), keylet, - args(A2.id(), 0, [&](Adjustements& sample) {})); + args(A2.id(), 0, [&](Adjustments& sample) {})); }, XRPAmount{}, STTx{ @@ -3364,7 +3425,7 @@ class Invariants_test : public beast::unit_test::suite return adjust( ac.view(), keylet, - args(A4.id(), 10, [&](Adjustements& sample) { + args(A4.id(), 10, [&](Adjustments& sample) { sample.sharesTotal = 0; })); }, @@ -3385,8 +3446,8 @@ class Invariants_test : public beast::unit_test::suite return adjust( ac.view(), keylet, - args(A4.id(), -10, [&](Adjustements& sample) { - sample.accountShares->amount = 0; + args(A4.id(), -10, [&](Adjustments& sample) { + sample.accountShares.reset(); })); }, XRPAmount{}, @@ -3408,7 +3469,7 @@ class Invariants_test : public beast::unit_test::suite return adjust( ac.view(), keylet, - args(A4.id(), -10, [&](Adjustements& sample) { + args(A4.id(), -10, [&](Adjustments& sample) { sample.accountShares->amount = -8; sample.assetsTotal = -7; sample.assetsAvailable = -7; diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 8b2254a840..bdebb70d09 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include @@ -43,6 +44,8 @@ #include #include +#include + namespace ripple { class Vault_test : public beast::unit_test::suite @@ -303,6 +306,55 @@ class Vault_test : public beast::unit_test::suite BEAST_EXPECT( env.balance(depositor, shares) == share(200 * scale)); } + else + { + testcase(prefix + " deposit/withdrawal same or less than fee"); + auto const amount = env.current()->fees().base; + + auto tx = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = amount}); + env(tx); + env.close(); + + tx = vault.withdraw( + {.depositor = depositor, + .id = keylet.key, + .amount = amount}); + env(tx); + env.close(); + + tx = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = amount}); + env(tx); + env.close(); + + // Withdraw to 3rd party + tx = vault.withdraw( + {.depositor = depositor, + .id = keylet.key, + .amount = amount}); + tx[sfDestination] = charlie.human(); + env(tx); + env.close(); + + tx = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = amount - 1}); + env(tx); + env.close(); + + tx = vault.withdraw( + {.depositor = depositor, + .id = keylet.key, + .amount = amount - 1}); + env(tx); + env.close(); + } { testcase( @@ -4795,6 +4847,147 @@ class Vault_test : public beast::unit_test::suite } } + void + testDelegate() + { + using namespace test::jtx; + + Env env(*this, testable_amendments()); + Account alice{"alice"}; + Account bob{"bob"}; + Account carol{"carol"}; + + struct CaseArgs + { + PrettyAsset asset = xrpIssue(); + }; + + auto const xrpBalance = + [this]( + Env const& env, Account const& account) -> std::optional { + auto sle = env.le(keylet::account(account.id())); + if (BEAST_EXPECT(sle != nullptr)) + return sle->getFieldAmount(sfBalance).xrp().drops(); + return std::nullopt; + }; + + auto testCase = [&, this](auto test, CaseArgs args = {}) { + Env env{*this, testable_amendments() | featureSingleAssetVault}; + + Vault vault{env}; + + // use different initial amount to distinguish the source balance + env.fund(XRP(10000), alice); + env.fund(XRP(20000), bob); + env.fund(XRP(30000), carol); + env.close(); + + env(delegate::set( + carol, + alice, + {"Payment", + "VaultCreate", + "VaultSet", + "VaultDelete", + "VaultDeposit", + "VaultWithdraw", + "VaultClawback"})); + + test(env, vault, args.asset); + }; + + testCase([&, this](Env& env, Vault& vault, PrettyAsset const& asset) { + testcase("delegated vault creation"); + auto startBalance = xrpBalance(env, carol); + if (!BEAST_EXPECT(startBalance.has_value())) + return; + + auto [tx, keylet] = vault.create({.owner = carol, .asset = asset}); + env(tx, delegate::as(alice)); + env.close(); + BEAST_EXPECT(xrpBalance(env, carol) == *startBalance); + }); + + testCase([&, this](Env& env, Vault& vault, PrettyAsset const& asset) { + testcase("delegated deposit and withdrawal"); + auto [tx, keylet] = vault.create({.owner = carol, .asset = asset}); + env(tx); + env.close(); + + auto const amount = 1513; + auto const baseFee = env.current()->fees().base; + + auto startBalance = xrpBalance(env, carol); + if (!BEAST_EXPECT(startBalance.has_value())) + return; + + tx = vault.deposit( + {.depositor = carol, + .id = keylet.key, + .amount = asset(amount)}); + env(tx, delegate::as(alice)); + env.close(); + BEAST_EXPECT(xrpBalance(env, carol) == *startBalance - amount); + + tx = vault.withdraw( + {.depositor = carol, + .id = keylet.key, + .amount = asset(amount - 1)}); + env(tx, delegate::as(alice)); + env.close(); + BEAST_EXPECT(xrpBalance(env, carol) == *startBalance - 1); + + tx = vault.withdraw( + {.depositor = carol, .id = keylet.key, .amount = asset(1)}); + env(tx); + env.close(); + BEAST_EXPECT(xrpBalance(env, carol) == *startBalance - baseFee); + }); + + testCase([&, this](Env& env, Vault& vault, PrettyAsset const& asset) { + testcase("delegated withdrawal same as base fee and deletion"); + auto [tx, keylet] = vault.create({.owner = carol, .asset = asset}); + env(tx); + env.close(); + + auto const amount = 25537; + auto const baseFee = env.current()->fees().base; + + auto startBalance = xrpBalance(env, carol); + if (!BEAST_EXPECT(startBalance.has_value())) + return; + + tx = vault.deposit( + {.depositor = carol, + .id = keylet.key, + .amount = asset(amount)}); + env(tx); + env.close(); + BEAST_EXPECT( + xrpBalance(env, carol) == *startBalance - amount - baseFee); + + tx = vault.withdraw( + {.depositor = carol, + .id = keylet.key, + .amount = asset(baseFee)}); + env(tx, delegate::as(alice)); + env.close(); + BEAST_EXPECT(xrpBalance(env, carol) == *startBalance - amount); + + tx = vault.withdraw( + {.depositor = carol, + .id = keylet.key, + .amount = asset(amount - baseFee)}); + env(tx, delegate::as(alice)); + env.close(); + BEAST_EXPECT(xrpBalance(env, carol) == *startBalance - baseFee); + + tx = vault.del({.owner = carol, .id = keylet.key}); + env(tx, delegate::as(alice)); + env.close(); + }); + } + public: void run() override @@ -4812,6 +5005,7 @@ public: testFailedPseudoAccount(); testScaleIOU(); testRPC(); + testDelegate(); } }; diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index 2cfcb6d258..f7f67ee1d6 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -2230,13 +2230,12 @@ ValidVault::visitEntry( after != nullptr && (before != nullptr || !isDelete), "ripple::ValidVault::visitEntry : some object is available"); - // `Number balance` will capture the difference (delta) between "before" + // Number balanceDelta will capture the difference (delta) between "before" // state (zero if created) and "after" state (zero if destroyed), so the // invariants can validate that the change in account balances matches the // change in vault balances, stored to deltas_ at the end of this function. - Number balance{}; + Number balanceDelta{}; - // By default do not add anything to deltas std::int8_t sign = 0; if (before) { @@ -2249,18 +2248,18 @@ ValidVault::visitEntry( // At this moment we have no way of telling if this object holds // vault shares or something else. Save it for finalize. beforeMPTs_.push_back(Shares::make(*before)); - balance = static_cast( + balanceDelta = static_cast( before->getFieldU64(sfOutstandingAmount)); sign = 1; break; case ltMPTOKEN: - balance = + balanceDelta = static_cast(before->getFieldU64(sfMPTAmount)); sign = -1; break; case ltACCOUNT_ROOT: case ltRIPPLE_STATE: - balance = before->getFieldAmount(sfBalance); + balanceDelta = before->getFieldAmount(sfBalance); sign = -1; break; default:; @@ -2278,18 +2277,18 @@ ValidVault::visitEntry( // At this moment we have no way of telling if this object holds // vault shares or something else. Save it for finalize. afterMPTs_.push_back(Shares::make(*after)); - balance -= Number(static_cast( + balanceDelta -= Number(static_cast( after->getFieldU64(sfOutstandingAmount))); sign = 1; break; case ltMPTOKEN: - balance -= Number( + balanceDelta -= Number( static_cast(after->getFieldU64(sfMPTAmount))); sign = -1; break; case ltACCOUNT_ROOT: case ltRIPPLE_STATE: - balance -= Number(after->getFieldAmount(sfBalance)); + balanceDelta -= Number(after->getFieldAmount(sfBalance)); sign = -1; break; default:; @@ -2297,8 +2296,13 @@ ValidVault::visitEntry( } uint256 const key = (before ? before->key() : after->key()); - if (sign && balance != zero) - deltas_[key] = balance * sign; + // Append to deltas if sign is non-zero, i.e. an object of an interesting + // type has been updated. A transaction may update an object even when + // its balance has not changed, e.g. transaction fee equals the amount + // transferred to the account. We intentionally do not compare balanceDelta + // against zero, to avoid missing such updates. + if (sign != 0) + deltas_[key] = balanceDelta * sign; } bool @@ -2604,6 +2608,23 @@ ValidVault::finalize( }, vaultAsset.value()); }; + auto const deltaAssetsTxAccount = [&]() -> std::optional { + auto ret = deltaAssets(tx[sfAccount]); + // Nothing returned or not XRP transaction + if (!ret.has_value() || !vaultAsset.native()) + return ret; + + // Delegated transaction; no need to compensate for fees + if (auto const delegate = tx[~sfDelegate]; + delegate.has_value() && *delegate != tx[sfAccount]) + return ret; + + *ret += fee.drops(); + if (*ret == zero) + return std::nullopt; + + return ret; + }; auto const deltaShares = [&](AccountID const& id) -> std::optional { auto const it = [&]() { if (id == afterVault.pseudoId) @@ -2774,20 +2795,7 @@ ValidVault::finalize( if (!issuerDeposit) { - auto const accountDeltaAssets = - [&]() -> std::optional { - if (auto ret = deltaAssets(tx[sfAccount]); ret) - { - // Compensate for transaction fee deduced from - // sfAccount - if (vaultAsset.native()) - *ret += fee.drops(); - if (*ret != zero) - return ret; - } - return std::nullopt; - }(); - + auto const accountDeltaAssets = deltaAssetsTxAccount(); if (!accountDeltaAssets) { JLOG(j.fatal()) << // @@ -2840,7 +2848,7 @@ ValidVault::finalize( } auto const vaultDeltaShares = deltaShares(afterVault.pseudoId); - if (!vaultDeltaShares) + if (!vaultDeltaShares || *vaultDeltaShares == zero) { JLOG(j.fatal()) << // "Invariant failed: deposit must change vault shares"; @@ -2909,20 +2917,7 @@ ValidVault::finalize( if (!issuerWithdrawal) { - auto const accountDeltaAssets = - [&]() -> std::optional { - if (auto ret = deltaAssets(tx[sfAccount]); ret) - { - // Compensate for transaction fee deduced from - // sfAccount - if (vaultAsset.native()) - *ret += fee.drops(); - if (*ret != zero) - return ret; - } - return std::nullopt; - }(); - + auto const accountDeltaAssets = deltaAssetsTxAccount(); auto const otherAccountDelta = [&]() -> std::optional { if (auto const destination = tx[~sfDestination]; @@ -2979,7 +2974,7 @@ ValidVault::finalize( } auto const vaultDeltaShares = deltaShares(afterVault.pseudoId); - if (!vaultDeltaShares) + if (!vaultDeltaShares || *vaultDeltaShares == zero) { JLOG(j.fatal()) << // "Invariant failed: withdrawal must change vault shares"; @@ -3064,7 +3059,7 @@ ValidVault::finalize( } auto const vaultDeltaShares = deltaShares(afterVault.pseudoId); - if (!vaultDeltaShares) + if (!vaultDeltaShares || *vaultDeltaShares == zero) { JLOG(j.fatal()) << // "Invariant failed: clawback must change vault shares"; From 0aa23933eabf1692ffaa2ecfc58b7eff849bbc0a Mon Sep 17 00:00:00 2001 From: Bart Date: Tue, 28 Oct 2025 15:52:13 -0400 Subject: [PATCH 11/28] ci: Only log into Conan when uploading packages (#5952) There are separate steps for logging into Conan and uploading packages. However, at the moment sometimes the login step is executed even though no packages will be uploaded. The condition for performing both steps should be the same. Co-authored-by: Bart Thomee <11445373+bthomee@users.noreply.github.com> --- .github/workflows/upload-conan-deps.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/upload-conan-deps.yml b/.github/workflows/upload-conan-deps.yml index 0bea36cea7..538280676b 100644 --- a/.github/workflows/upload-conan-deps.yml +++ b/.github/workflows/upload-conan-deps.yml @@ -84,11 +84,11 @@ jobs: verbosity: ${{ runner.os == 'Windows' && 'quiet' || 'verbose' }} - name: Log into Conan remote - if: ${{ github.repository_owner == 'XRPLF' && github.event_name != 'pull_request' }} + if: ${{ github.repository_owner == 'XRPLF' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }} run: conan remote login "${CONAN_REMOTE_NAME}" "${{ secrets.CONAN_REMOTE_USERNAME }}" --password "${{ secrets.CONAN_REMOTE_PASSWORD }}" - name: Upload Conan packages - if: ${{ github.repository_owner == 'XRPLF' && github.event_name != 'pull_request' && github.event_name != 'schedule' }} + if: ${{ github.repository_owner == 'XRPLF' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }} env: FORCE_OPTION: ${{ github.event.inputs.force_upload == 'true' && '--force' || '' }} run: conan upload "*" --remote="${CONAN_REMOTE_NAME}" --confirm ${FORCE_OPTION} From 5efaf0c328e4ccb5d4b9e0974437f6a6e551274d Mon Sep 17 00:00:00 2001 From: Bart Date: Tue, 28 Oct 2025 15:52:52 -0400 Subject: [PATCH 12/28] ci: Only upload codecov reports in the original repo, not in forks (#5953) Co-authored-by: Bart Thomee <11445373+bthomee@users.noreply.github.com> --- .github/workflows/reusable-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/reusable-build.yml b/.github/workflows/reusable-build.yml index 09f7f25271..8aa84608d4 100644 --- a/.github/workflows/reusable-build.yml +++ b/.github/workflows/reusable-build.yml @@ -126,7 +126,7 @@ jobs: if-no-files-found: error - name: Upload coverage report - if: ${{ inputs.cmake_target == 'coverage' }} + if: ${{ github.repository_owner == 'XRPLF' && inputs.cmake_target == 'coverage' }} uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 with: disable_search: true From 76611c3f46b3a063750aaa83cc4551128e06e7b5 Mon Sep 17 00:00:00 2001 From: Bart Date: Tue, 28 Oct 2025 16:08:08 -0400 Subject: [PATCH 13/28] ci: Use commit hash so workflows are not canceled when merging multiple PRs (#5950) This change changes the CI concurrency group for pushes to the `develop` branch to use the commit hash instead of the target branch. Co-authored-by: Bart Thomee <11445373+bthomee@users.noreply.github.com> --- .github/workflows/on-trigger.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/on-trigger.yml b/.github/workflows/on-trigger.yml index c1f6839d2d..9df6417c07 100644 --- a/.github/workflows/on-trigger.yml +++ b/.github/workflows/on-trigger.yml @@ -50,7 +50,12 @@ on: workflow_dispatch: concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + # When a PR is merged into the develop branch it will be assigned a unique + # group identifier, so execution will continue even if another PR is merged + # while it is still running. In all other cases the group identifier is shared + # per branch, so that any in-progress runs are cancelled when a new commit is + # pushed. + group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' && github.sha || github.ref }} cancel-in-progress: true defaults: From 1dd60242de5d9b8038b4bd7a65b97760dd1b7f96 Mon Sep 17 00:00:00 2001 From: Bart Date: Tue, 28 Oct 2025 20:07:09 -0400 Subject: [PATCH 14/28] ci: Use nproc-2 to set parallelism for builds and tests (#5939) This change reduces the number of cores used to build and test, as using all cores may be contributing to occasional build and test failures. Co-authored-by: Bart Thomee <11445373+bthomee@users.noreply.github.com> --- .github/actions/build-deps/action.yml | 19 ++++++++++------- .github/workflows/publish-docs.yml | 14 ++++++++++++- .../workflows/reusable-build-test-config.yml | 8 +++++++ .github/workflows/reusable-build.yml | 18 +++++++++++++++- .github/workflows/reusable-test.yml | 15 ++++++++++++- .github/workflows/upload-conan-deps.yml | 21 +++++++++++++++---- conan/global.conf | 3 +-- 7 files changed, 82 insertions(+), 16 deletions(-) diff --git a/.github/actions/build-deps/action.yml b/.github/actions/build-deps/action.yml index d5897bc52a..f20eb3a595 100644 --- a/.github/actions/build-deps/action.yml +++ b/.github/actions/build-deps/action.yml @@ -4,20 +4,23 @@ description: "Install Conan dependencies, optionally forcing a rebuild of all de # Note that actions do not support 'type' and all inputs are strings, see # https://docs.github.com/en/actions/reference/workflows-and-actions/metadata-syntax#inputs. inputs: - verbosity: - description: "The build verbosity." - required: false - default: "verbose" build_dir: description: "The directory where to build." required: true build_type: description: 'The build type to use ("Debug", "Release").' required: true + build_nproc: + description: "The number of processors to use for building." + required: true force_build: description: 'Force building of all dependencies ("true", "false").' required: false default: "false" + log_verbosity: + description: "The logging verbosity." + required: false + default: "verbose" runs: using: composite @@ -26,9 +29,10 @@ runs: shell: bash env: BUILD_DIR: ${{ inputs.build_dir }} + BUILD_NPROC: ${{ inputs.build_nproc }} BUILD_OPTION: ${{ inputs.force_build == 'true' && '*' || 'missing' }} BUILD_TYPE: ${{ inputs.build_type }} - VERBOSITY: ${{ inputs.verbosity }} + LOG_VERBOSITY: ${{ inputs.log_verbosity }} run: | echo 'Installing dependencies.' mkdir -p "${BUILD_DIR}" @@ -39,6 +43,7 @@ runs: --options:host='&:tests=True' \ --options:host='&:xrpld=True' \ --settings:all build_type="${BUILD_TYPE}" \ - --conf:all tools.build:verbosity="${VERBOSITY}" \ - --conf:all tools.compilation:verbosity="${VERBOSITY}" \ + --conf:all tools.build:jobs=${BUILD_NPROC} \ + --conf:all tools.build:verbosity="${LOG_VERBOSITY}" \ + --conf:all tools.compilation:verbosity="${LOG_VERBOSITY}" \ .. diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index 46d377a41d..14a2ba2fc0 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -23,6 +23,7 @@ defaults: env: BUILD_DIR: .build + NPROC_SUBTRACT: 2 jobs: publish: @@ -33,6 +34,13 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + + - name: Get number of processors + uses: XRPLF/actions/.github/actions/get-nproc@046b1620f6bfd6cd0985dc82c3df02786801fe0a + id: nproc + with: + subtract: ${{ env.NPROC_SUBTRACT }} + - name: Check configuration run: | echo 'Checking path.' @@ -46,12 +54,16 @@ jobs: echo 'Checking Doxygen version.' doxygen --version + - name: Build documentation + env: + BUILD_NPROC: ${{ steps.nproc.outputs.nproc }} run: | mkdir -p "${BUILD_DIR}" cd "${BUILD_DIR}" cmake -Donly_docs=ON .. - cmake --build . --target docs --parallel $(nproc) + cmake --build . --target docs --parallel ${BUILD_NPROC} + - name: Publish documentation if: ${{ github.ref_type == 'branch' && github.ref_name == github.event.repository.default_branch }} uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0 diff --git a/.github/workflows/reusable-build-test-config.yml b/.github/workflows/reusable-build-test-config.yml index 3160cef031..a59dbda71b 100644 --- a/.github/workflows/reusable-build-test-config.yml +++ b/.github/workflows/reusable-build-test-config.yml @@ -39,6 +39,12 @@ on: required: true type: string + nproc_subtract: + description: "The number of processors to subtract when calculating parallelism." + required: false + type: number + default: 2 + secrets: CODECOV_TOKEN: description: "The Codecov token to use for uploading coverage reports." @@ -55,6 +61,7 @@ jobs: runs_on: ${{ inputs.runs_on }} image: ${{ inputs.image }} config_name: ${{ inputs.config_name }} + nproc_subtract: ${{ inputs.nproc_subtract }} secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} @@ -67,3 +74,4 @@ jobs: runs_on: ${{ inputs.runs_on }} image: ${{ inputs.image }} config_name: ${{ inputs.config_name }} + nproc_subtract: ${{ inputs.nproc_subtract }} diff --git a/.github/workflows/reusable-build.yml b/.github/workflows/reusable-build.yml index 8aa84608d4..d156f05394 100644 --- a/.github/workflows/reusable-build.yml +++ b/.github/workflows/reusable-build.yml @@ -34,6 +34,11 @@ on: required: true type: string + nproc_subtract: + description: "The number of processors to subtract when calculating parallelism." + required: true + type: number + secrets: CODECOV_TOKEN: description: "The Codecov token to use for uploading coverage reports." @@ -65,6 +70,12 @@ jobs: - name: Print build environment uses: ./.github/actions/print-env + - name: Get number of processors + uses: XRPLF/actions/.github/actions/get-nproc@046b1620f6bfd6cd0985dc82c3df02786801fe0a + id: nproc + with: + subtract: ${{ inputs.nproc_subtract }} + - name: Setup Conan uses: ./.github/actions/setup-conan @@ -72,7 +83,11 @@ jobs: uses: ./.github/actions/build-deps with: build_dir: ${{ inputs.build_dir }} + build_nproc: ${{ steps.nproc.outputs.nproc }} build_type: ${{ inputs.build_type }} + # Set the verbosity to "quiet" for Windows to avoid an excessive + # amount of logs. For other OSes, the "verbose" logs are more useful. + log_verbosity: ${{ runner.os == 'Windows' && 'quiet' || 'verbose' }} - name: Configure CMake shell: bash @@ -92,13 +107,14 @@ jobs: shell: bash working-directory: ${{ inputs.build_dir }} env: + BUILD_NPROC: ${{ steps.nproc.outputs.nproc }} BUILD_TYPE: ${{ inputs.build_type }} CMAKE_TARGET: ${{ inputs.cmake_target }} run: | cmake \ --build . \ --config "${BUILD_TYPE}" \ - --parallel $(nproc) \ + --parallel ${BUILD_NPROC} \ --target "${CMAKE_TARGET}" - name: Put built binaries in one location diff --git a/.github/workflows/reusable-test.yml b/.github/workflows/reusable-test.yml index 4b9c75334a..2127355f40 100644 --- a/.github/workflows/reusable-test.yml +++ b/.github/workflows/reusable-test.yml @@ -26,6 +26,11 @@ on: required: true type: string + nproc_subtract: + description: "The number of processors to subtract when calculating parallelism." + required: true + type: number + jobs: test: name: Test ${{ inputs.config_name }} @@ -37,6 +42,12 @@ jobs: if: ${{ runner.os == 'macOS' }} uses: XRPLF/actions/.github/actions/cleanup-workspace@3f044c7478548e3c32ff68980eeb36ece02b364e + - name: Get number of processors + uses: XRPLF/actions/.github/actions/get-nproc@046b1620f6bfd6cd0985dc82c3df02786801fe0a + id: nproc + with: + subtract: ${{ inputs.nproc_subtract }} + - name: Download rippled artifact uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: @@ -69,8 +80,10 @@ jobs: - name: Run the embedded tests if: ${{ inputs.run_tests }} shell: bash + env: + BUILD_NPROC: ${{ steps.nproc.outputs.nproc }} run: | - ./rippled --unittest --unittest-jobs $(nproc) + ./rippled --unittest --unittest-jobs ${BUILD_NPROC} - name: Run the separate tests if: ${{ inputs.run_tests }} diff --git a/.github/workflows/upload-conan-deps.yml b/.github/workflows/upload-conan-deps.yml index 538280676b..aa3870ffcf 100644 --- a/.github/workflows/upload-conan-deps.yml +++ b/.github/workflows/upload-conan-deps.yml @@ -34,6 +34,7 @@ on: env: CONAN_REMOTE_NAME: xrplf CONAN_REMOTE_URL: https://conan.ripplex.io + NPROC_SUBTRACT: 2 concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -61,12 +62,23 @@ jobs: if: ${{ runner.os == 'macOS' }} uses: XRPLF/actions/.github/actions/cleanup-workspace@3f044c7478548e3c32ff68980eeb36ece02b364e - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - name: Checkout repository + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - name: Prepare runner uses: XRPLF/actions/.github/actions/prepare-runner@638e0dc11ea230f91bd26622fb542116bb5254d5 with: disable_ccache: false + - name: Print build environment + uses: ./.github/actions/print-env + + - name: Get number of processors + uses: XRPLF/actions/.github/actions/get-nproc@046b1620f6bfd6cd0985dc82c3df02786801fe0a + id: nproc + with: + subtract: ${{ env.NPROC_SUBTRACT }} + - name: Setup Conan uses: ./.github/actions/setup-conan with: @@ -77,11 +89,12 @@ jobs: uses: ./.github/actions/build-deps with: build_dir: .build + build_nproc: ${{ steps.nproc.outputs.nproc }} build_type: ${{ matrix.build_type }} force_build: ${{ github.event_name == 'schedule' || github.event.inputs.force_source_build == 'true' }} - # The verbosity is set to "quiet" for Windows to avoid an excessive amount of logs, while it - # is set to "verbose" otherwise to provide more information during the build process. - verbosity: ${{ runner.os == 'Windows' && 'quiet' || 'verbose' }} + # Set the verbosity to "quiet" for Windows to avoid an excessive + # amount of logs. For other OSes, the "verbose" logs are more useful. + log_verbosity: ${{ runner.os == 'Windows' && 'quiet' || 'verbose' }} - name: Log into Conan remote if: ${{ github.repository_owner == 'XRPLF' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }} diff --git a/conan/global.conf b/conan/global.conf index a184adf629..37b329a5c5 100644 --- a/conan/global.conf +++ b/conan/global.conf @@ -1,6 +1,5 @@ # Global configuration for Conan. This is used to set the number of parallel -# downloads, uploads, and build jobs. +# downloads and uploads. core:non_interactive=True core.download:parallel={{ os.cpu_count() }} core.upload:parallel={{ os.cpu_count() }} -tools.build:jobs={{ os.cpu_count() - 1 }} From a8e4da0b116389f4878ac757e0e9ea9141df7339 Mon Sep 17 00:00:00 2001 From: Pratik Mankawde Date: Wed, 29 Oct 2025 11:32:43 +0000 Subject: [PATCH 15/28] Retire fix1781 amendment (#5931) * Retired fix1781 amendment Signed-off-by: Pratik Mankawde * refactor: Retire fix1781 amendment Amendments activated for more than 2 years can be retired. This change retires the fix1781 amendment. --------- Signed-off-by: Pratik Mankawde Co-authored-by: Bart Thomee <11445373+bthomee@users.noreply.github.com> --- include/xrpl/protocol/detail/features.macro | 3 +- src/test/app/AMMExtended_test.cpp | 11 +--- src/test/app/Flow_test.cpp | 50 ++++++++----------- .../app/paths/detail/XRPEndpointStep.cpp | 14 ++---- 4 files changed, 29 insertions(+), 49 deletions(-) diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 3e3309b444..6670b5fb0b 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -98,8 +98,6 @@ XRPL_FEATURE(TicketBatch, Supported::yes, VoteBehavior::DefaultYe XRPL_FEATURE(NegativeUNL, Supported::yes, VoteBehavior::DefaultYes) XRPL_FIX (AmendmentMajorityCalc, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(HardenedValidations, Supported::yes, VoteBehavior::DefaultYes) -// fix1781: XRPEndpointSteps should be included in the circular payment check -XRPL_FIX (1781, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(RequireFullyCanonicalSig, Supported::yes, VoteBehavior::DefaultYes) XRPL_FIX (QualityUpperBound, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(DeletableAccounts, Supported::yes, VoteBehavior::DefaultYes) @@ -155,3 +153,4 @@ XRPL_RETIRE(FlowCross) XRPL_RETIRE(fix1513) XRPL_RETIRE(fix1515) XRPL_RETIRE(fix1543) +XRPL_RETIRE(fix1781) diff --git a/src/test/app/AMMExtended_test.cpp b/src/test/app/AMMExtended_test.cpp index dcbebd2dc3..ee6988bd75 100644 --- a/src/test/app/AMMExtended_test.cpp +++ b/src/test/app/AMMExtended_test.cpp @@ -2819,15 +2819,9 @@ private: testcase("Circular XRP"); using namespace jtx; - - for (auto const withFix : {true, false}) { - auto const feats = withFix - ? testable_amendments() - : testable_amendments() - FeatureBitset{fix1781}; - // Payment path starting with XRP - Env env(*this, feats); + Env env(*this, testable_amendments()); // Note, if alice doesn't have default ripple, then pay // fails with tecPATH_DRY. fund( @@ -2842,8 +2836,7 @@ private: AMM ammAliceXRP_EUR(env, alice, XRP(100), EUR(101)); env.close(); - TER const expectedTer = - withFix ? TER{temBAD_PATH_LOOP} : TER{tesSUCCESS}; + TER const expectedTer = TER{temBAD_PATH_LOOP}; env(pay(alice, bob, EUR(1)), path(~USD, ~XRP, ~EUR), sendmax(XRP(1)), diff --git a/src/test/app/Flow_test.cpp b/src/test/app/Flow_test.cpp index 64033ba86d..6c4331c7c8 100644 --- a/src/test/app/Flow_test.cpp +++ b/src/test/app/Flow_test.cpp @@ -1190,38 +1190,30 @@ struct Flow_test : public beast::unit_test::suite auto const USD = gw["USD"]; auto const EUR = gw["EUR"]; - for (auto const withFix : {true, false}) { - auto const feats = [&withFix]() -> FeatureBitset { - if (withFix) - return testable_amendments(); - return testable_amendments() - FeatureBitset{fix1781}; - }(); - { - // Payment path starting with XRP - Env env(*this, feats); - env.fund(XRP(10000), alice, bob, gw); - env.close(); - env.trust(USD(1000), alice, bob); - env.trust(EUR(1000), alice, bob); - env.close(); - env(pay(gw, alice, USD(100))); - env(pay(gw, alice, EUR(100))); - env.close(); + // Payment path starting with XRP + Env env(*this, testable_amendments()); + env.fund(XRP(10000), alice, bob, gw); + env.close(); + env.trust(USD(1000), alice, bob); + env.trust(EUR(1000), alice, bob); + env.close(); + env(pay(gw, alice, USD(100))); + env(pay(gw, alice, EUR(100))); + env.close(); - env(offer(alice, XRP(100), USD(100)), txflags(tfPassive)); - env(offer(alice, USD(100), XRP(100)), txflags(tfPassive)); - env(offer(alice, XRP(100), EUR(100)), txflags(tfPassive)); - env.close(); + env(offer(alice, XRP(100), USD(100)), txflags(tfPassive)); + env(offer(alice, USD(100), XRP(100)), txflags(tfPassive)); + env(offer(alice, XRP(100), EUR(100)), txflags(tfPassive)); + env.close(); + + TER const expectedTer = TER{temBAD_PATH_LOOP}; + env(pay(alice, bob, EUR(1)), + path(~USD, ~XRP, ~EUR), + sendmax(XRP(1)), + txflags(tfNoRippleDirect), + ter(expectedTer)); - TER const expectedTer = - withFix ? TER{temBAD_PATH_LOOP} : TER{tesSUCCESS}; - env(pay(alice, bob, EUR(1)), - path(~USD, ~XRP, ~EUR), - sendmax(XRP(1)), - txflags(tfNoRippleDirect), - ter(expectedTer)); - } pass(); } { diff --git a/src/xrpld/app/paths/detail/XRPEndpointStep.cpp b/src/xrpld/app/paths/detail/XRPEndpointStep.cpp index 9cbcb0c84d..595b596c5b 100644 --- a/src/xrpld/app/paths/detail/XRPEndpointStep.cpp +++ b/src/xrpld/app/paths/detail/XRPEndpointStep.cpp @@ -362,16 +362,12 @@ XRPEndpointStep::check(StrandContext const& ctx) const if (ter != tesSUCCESS) return ter; - if (ctx.view.rules().enabled(fix1781)) + auto const issuesIndex = isLast_ ? 0 : 1; + if (!ctx.seenDirectIssues[issuesIndex].insert(xrpIssue()).second) { - auto const issuesIndex = isLast_ ? 0 : 1; - if (!ctx.seenDirectIssues[issuesIndex].insert(xrpIssue()).second) - { - JLOG(j_.debug()) - << "XRPEndpointStep: loop detected: Index: " << ctx.strandSize - << ' ' << *this; - return temBAD_PATH_LOOP; - } + JLOG(j_.debug()) << "XRPEndpointStep: loop detected: Index: " + << ctx.strandSize << ' ' << *this; + return temBAD_PATH_LOOP; } return tesSUCCESS; From ed5d6f3e22e7806545ff299ed6329e3292d186fb Mon Sep 17 00:00:00 2001 From: Jingchen Date: Wed, 29 Oct 2025 14:16:37 +0000 Subject: [PATCH 16/28] feat: Add public key to log messages (#5678) To protect the identity of UNL validators, the IP addresses are redacted from the log messages sent to the common Grafana instance. However, without such identifying information it is challenging to debug issues. This change adds a node's public key to logs to improve our ability to debug issues. Co-authored-by: Bart Thomee <11445373+bthomee@users.noreply.github.com> --- include/xrpl/protocol/PublicKey.h | 19 +++++ include/xrpl/resource/Consumer.h | 4 + include/xrpl/resource/detail/Entry.h | 5 +- src/libxrpl/resource/Consumer.cpp | 6 ++ src/test/app/LedgerReplay_test.cpp | 7 ++ src/test/overlay/reduce_relay_test.cpp | 7 ++ src/xrpld/app/misc/detail/ValidatorList.cpp | 7 +- src/xrpld/overlay/Peer.h | 2 + src/xrpld/overlay/detail/ConnectAttempt.cpp | 2 + src/xrpld/overlay/detail/OverlayImpl.cpp | 20 ++--- src/xrpld/overlay/detail/PeerImp.cpp | 85 +++++++++------------ src/xrpld/overlay/detail/PeerImp.h | 23 +++++- src/xrpld/peerfinder/detail/Logic.h | 72 ++++++++--------- src/xrpld/peerfinder/detail/SlotImp.h | 6 ++ 14 files changed, 162 insertions(+), 103 deletions(-) diff --git a/include/xrpl/protocol/PublicKey.h b/include/xrpl/protocol/PublicKey.h index 9bf01e5cda..a54f593fee 100644 --- a/include/xrpl/protocol/PublicKey.h +++ b/include/xrpl/protocol/PublicKey.h @@ -21,6 +21,7 @@ #define RIPPLE_PROTOCOL_PUBLICKEY_H_INCLUDED #include +#include #include #include #include @@ -264,6 +265,24 @@ calcNodeID(PublicKey const&); AccountID calcAccountID(PublicKey const& pk); +inline std::string +getFingerprint( + beast::IP::Endpoint const& address, + std::optional const& publicKey = std::nullopt, + std::optional const& id = std::nullopt) +{ + std::stringstream ss; + ss << "IP Address: " << address; + if (publicKey.has_value()) + { + ss << ", Public Key: " << toBase58(TokenType::NodePublic, *publicKey); + } + if (id.has_value()) + { + ss << ", Id: " << id.value(); + } + return ss.str(); +} } // namespace ripple //------------------------------------------------------------------------------ diff --git a/include/xrpl/resource/Consumer.h b/include/xrpl/resource/Consumer.h index 00d8a7b58b..cf6c25a205 100644 --- a/include/xrpl/resource/Consumer.h +++ b/include/xrpl/resource/Consumer.h @@ -21,6 +21,7 @@ #define RIPPLE_RESOURCE_CONSUMER_H_INCLUDED #include +#include #include #include @@ -87,6 +88,9 @@ public: Entry& entry(); + void + setPublicKey(PublicKey const& publicKey); + private: Logic* m_logic; Entry* m_entry; diff --git a/include/xrpl/resource/detail/Entry.h b/include/xrpl/resource/detail/Entry.h index 32ba77c6d3..a8002611ce 100644 --- a/include/xrpl/resource/detail/Entry.h +++ b/include/xrpl/resource/detail/Entry.h @@ -53,7 +53,7 @@ struct Entry : public beast::List::Node std::string to_string() const { - return key->address.to_string(); + return getFingerprint(key->address, publicKey); } /** @@ -82,6 +82,9 @@ struct Entry : public beast::List::Node return local_balance.add(charge, now) + remote_balance; } + // The public key of the peer + std::optional publicKey; + // Back pointer to the map key (bit of a hack here) Key const* key; diff --git a/src/libxrpl/resource/Consumer.cpp b/src/libxrpl/resource/Consumer.cpp index 71a84340cd..9ba3345f28 100644 --- a/src/libxrpl/resource/Consumer.cpp +++ b/src/libxrpl/resource/Consumer.cpp @@ -148,6 +148,12 @@ Consumer::entry() return *m_entry; } +void +Consumer::setPublicKey(PublicKey const& publicKey) +{ + m_entry->publicKey = publicKey; +} + std::ostream& operator<<(std::ostream& os, Consumer const& v) { diff --git a/src/test/app/LedgerReplay_test.cpp b/src/test/app/LedgerReplay_test.cpp index 88d944d789..e54f24d340 100644 --- a/src/test/app/LedgerReplay_test.cpp +++ b/src/test/app/LedgerReplay_test.cpp @@ -325,6 +325,13 @@ public: return false; } + std::string const& + fingerprint() const override + { + return fingerprint_; + } + + std::string fingerprint_; bool ledgerReplayEnabled_; PublicKey nodePublicKey_; }; diff --git a/src/test/overlay/reduce_relay_test.cpp b/src/test/overlay/reduce_relay_test.cpp index e53f53f2db..318eae21ec 100644 --- a/src/test/overlay/reduce_relay_test.cpp +++ b/src/test/overlay/reduce_relay_test.cpp @@ -475,6 +475,12 @@ public: return id_; } + std::string const& + fingerprint() const override + { + return fingerprint_; + } + static void resetId() { @@ -508,6 +514,7 @@ public: private: inline static id_t sid_ = 0; + std::string fingerprint_; id_t id_; Overlay& overlay_; reduce_relay::Squelch squelch_; diff --git a/src/xrpld/app/misc/detail/ValidatorList.cpp b/src/xrpld/app/misc/detail/ValidatorList.cpp index 92095b7211..926649808d 100644 --- a/src/xrpld/app/misc/detail/ValidatorList.cpp +++ b/src/xrpld/app/misc/detail/ValidatorList.cpp @@ -795,9 +795,7 @@ ValidatorList::sendValidatorList( << " validator list collection(s) containing " << numVLs << " validator list(s) for " << strHex(publisherKey) << " with sequence range " << peerSequence << ", " - << newPeerSequence << " to " - << peer.getRemoteAddress().to_string() << " [" << peer.id() - << "]"; + << newPeerSequence << " to " << peer.fingerprint(); else { XRPL_ASSERT( @@ -807,8 +805,7 @@ ValidatorList::sendValidatorList( JLOG(j.debug()) << "Sent validator list for " << strHex(publisherKey) << " with sequence " << newPeerSequence << " to " - << peer.getRemoteAddress().to_string() << " [" << peer.id() - << "]"; + << peer.fingerprint(); } } } diff --git a/src/xrpld/overlay/Peer.h b/src/xrpld/overlay/Peer.h index 9a5bd7136b..9a1c75c9b8 100644 --- a/src/xrpld/overlay/Peer.h +++ b/src/xrpld/overlay/Peer.h @@ -112,6 +112,8 @@ public: virtual void setPublisherListSequence(PublicKey const&, std::size_t const) = 0; + virtual std::string const& + fingerprint() const = 0; // // Ledger // diff --git a/src/xrpld/overlay/detail/ConnectAttempt.cpp b/src/xrpld/overlay/detail/ConnectAttempt.cpp index 15a3b91802..3d7f0abe01 100644 --- a/src/xrpld/overlay/detail/ConnectAttempt.cpp +++ b/src/xrpld/overlay/detail/ConnectAttempt.cpp @@ -589,6 +589,8 @@ ConnectAttempt::processResponse() remote_endpoint_.address(), app_); + usage_.setPublicKey(publicKey); + JLOG(journal_.debug()) << "Protocol: " << to_string(*negotiatedProtocol); JLOG(journal_.info()) diff --git a/src/xrpld/overlay/detail/OverlayImpl.cpp b/src/xrpld/overlay/detail/OverlayImpl.cpp index 8d295faace..025fcfabce 100644 --- a/src/xrpld/overlay/detail/OverlayImpl.cpp +++ b/src/xrpld/overlay/detail/OverlayImpl.cpp @@ -262,6 +262,8 @@ OverlayImpl::onHandoff( remote_endpoint.address(), app_); + consumer.setPublicKey(publicKey); + { // The node gets a reserved slot if it is in our cluster // or if it has a reservation. @@ -434,6 +436,9 @@ OverlayImpl::connect(beast::IP::Endpoint const& remote_endpoint) void OverlayImpl::add_active(std::shared_ptr const& peer) { + beast::WrappedSink sink{journal_.sink(), peer->prefix()}; + beast::Journal journal{sink}; + std::lock_guard lock(mutex_); { @@ -457,11 +462,7 @@ OverlayImpl::add_active(std::shared_ptr const& peer) list_.emplace(peer.get(), peer); - JLOG(journal_.debug()) << "activated " << peer->getRemoteAddress() << " (" - << peer->id() << ":" - << toBase58( - TokenType::NodePublic, peer->getNodePublic()) - << ")"; + JLOG(journal.debug()) << "activated"; // As we are not on the strand, run() must be called // while holding the lock, otherwise new I/O can be @@ -605,6 +606,9 @@ OverlayImpl::onWrite(beast::PropertyStream::Map& stream) void OverlayImpl::activate(std::shared_ptr const& peer) { + beast::WrappedSink sink{journal_.sink(), peer->prefix()}; + beast::Journal journal{sink}; + // Now track this peer { std::lock_guard lock(mutex_); @@ -618,11 +622,7 @@ OverlayImpl::activate(std::shared_ptr const& peer) (void)result.second; } - JLOG(journal_.debug()) << "activated " << peer->getRemoteAddress() << " (" - << peer->id() << ":" - << toBase58( - TokenType::NodePublic, peer->getNodePublic()) - << ")"; + JLOG(journal.debug()) << "activated"; // We just accepted this peer so we have non-zero active peers XRPL_ASSERT(size(), "ripple::OverlayImpl::activate : nonzero peers"); diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index 47d01eb7c5..41491a2ec2 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -82,8 +82,11 @@ PeerImp::PeerImp( : Child(overlay) , app_(app) , id_(id) - , sink_(app_.journal("Peer"), makePrefix(id)) - , p_sink_(app_.journal("Protocol"), makePrefix(id)) + , fingerprint_( + getFingerprint(slot->remote_endpoint(), publicKey, to_string(id))) + , prefix_(makePrefix(fingerprint_)) + , sink_(app_.journal("Peer"), prefix_) + , p_sink_(app_.journal("Protocol"), prefix_) , journal_(sink_) , p_journal_(p_sink_) , stream_ptr_(std::move(stream_ptr)) @@ -131,8 +134,7 @@ PeerImp::PeerImp( headers_, FEATURE_VPRR, app_.config().VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE) - << " tx reduce-relay enabled " << txReduceRelayEnabled_ << " on " - << remote_address_ << " " << id_; + << " tx reduce-relay enabled " << txReduceRelayEnabled_; } PeerImp::~PeerImp() @@ -280,8 +282,7 @@ PeerImp::send(std::shared_ptr const& m) sink && (sendq_size % Tuning::sendQueueLogFreq) == 0) { std::string const n = name(); - sink << (n.empty() ? remote_address_.to_string() : n) - << " sendq: " << sendq_size; + sink << n << " sendq: " << sendq_size; } send_queue_.push(m); @@ -585,10 +586,7 @@ PeerImp::fail(std::string const& name, error_code ec) if (!socket_.is_open()) return; - JLOG(journal_.warn()) << name << " from " - << toBase58(TokenType::NodePublic, publicKey_) - << " at " << remote_address_.to_string() << ": " - << ec.message(); + JLOG(journal_.warn()) << name << ": " << ec.message(); shutdown(); } @@ -611,8 +609,7 @@ PeerImp::fail(std::string const& reason) if (journal_.active(beast::severities::kWarning)) { std::string const n = name(); - JLOG(journal_.warn()) << (n.empty() ? remote_address_.to_string() : n) - << " failed: " << reason; + JLOG(journal_.warn()) << n << " failed: " << reason; } shutdown(); @@ -730,6 +727,16 @@ PeerImp::setTimer(std::chrono::seconds interval) &PeerImp::onTimer, shared_from_this(), std::placeholders::_1))); } +//------------------------------------------------------------------------------ + +std::string +PeerImp::makePrefix(std::string const& fingerprint) +{ + std::stringstream ss; + ss << "[" << fingerprint << "] "; + return ss.str(); +} + void PeerImp::onTimer(error_code const& ec) { @@ -810,14 +817,6 @@ PeerImp::cancelTimer() noexcept } } -std::string -PeerImp::makePrefix(id_t id) -{ - std::stringstream ss; - ss << "[" << std::setfill('0') << std::setw(3) << id << "] "; - return ss.str(); -} - //------------------------------------------------------------------------------ void PeerImp::doAccept() @@ -826,7 +825,7 @@ PeerImp::doAccept() read_buffer_.size() == 0, "ripple::PeerImp::doAccept : empty read buffer"); - JLOG(journal_.debug()) << "doAccept: " << remote_address_; + JLOG(journal_.debug()) << "doAccept"; // a shutdown was initiated before the handshake, there is nothing to do if (shutdown_) @@ -840,8 +839,6 @@ PeerImp::doAccept() return fail("makeSharedValue: Unexpected failure"); JLOG(journal_.debug()) << "Protocol: " << to_string(protocol_); - JLOG(journal_.info()) << "Public Key: " - << toBase58(TokenType::NodePublic, publicKey_); if (auto member = app_.cluster().member(publicKey_)) { @@ -2143,8 +2140,7 @@ PeerImp::onValidatorListMessage( // ValidatorList class rules), so charge accordingly and skip processing. if (blobs.empty()) { - JLOG(p_journal_.warn()) << "Ignored malformed " << messageType - << " from peer " << remote_address_; + JLOG(p_journal_.warn()) << "Ignored malformed " << messageType; // This shouldn't ever happen with a well-behaved peer fee_.update(Resource::feeHeavyBurdenPeer, "no blobs"); return; @@ -2152,9 +2148,7 @@ PeerImp::onValidatorListMessage( auto const hash = sha512Half(manifest, blobs, version); - JLOG(p_journal_.debug()) - << "Received " << messageType << " from " << remote_address_.to_string() - << " (" << id_ << ")"; + JLOG(p_journal_.debug()) << "Received " << messageType; if (!app_.getHashRouter().addSuppressionPeer(hash, id_)) { @@ -2181,8 +2175,7 @@ PeerImp::onValidatorListMessage( << "Processed " << messageType << " version " << version << " from " << (applyResult.publisherKey ? strHex(*applyResult.publisherKey) : "unknown or invalid publisher") - << " from " << remote_address_.to_string() << " (" << id_ - << ") with best result " << to_string(applyResult.bestDisposition()); + << " with best result " << to_string(applyResult.bestDisposition()); // Act based on the best result switch (applyResult.bestDisposition()) @@ -2296,51 +2289,44 @@ PeerImp::onValidatorListMessage( // New list case ListDisposition::accepted: JLOG(p_journal_.debug()) - << "Applied " << count << " new " << messageType - << "(s) from peer " << remote_address_; + << "Applied " << count << " new " << messageType; break; // Newest list is expired, and that needs to be broadcast, too case ListDisposition::expired: JLOG(p_journal_.debug()) - << "Applied " << count << " expired " << messageType - << "(s) from peer " << remote_address_; + << "Applied " << count << " expired " << messageType; break; // Future list case ListDisposition::pending: JLOG(p_journal_.debug()) - << "Processed " << count << " future " << messageType - << "(s) from peer " << remote_address_; + << "Processed " << count << " future " << messageType; break; case ListDisposition::same_sequence: JLOG(p_journal_.warn()) << "Ignored " << count << " " << messageType - << "(s) with current sequence from peer " - << remote_address_; + << "(s) with current sequence"; break; case ListDisposition::known_sequence: JLOG(p_journal_.warn()) << "Ignored " << count << " " << messageType - << "(s) with future sequence from peer " << remote_address_; + << "(s) with future sequence"; break; case ListDisposition::stale: JLOG(p_journal_.warn()) - << "Ignored " << count << "stale " << messageType - << "(s) from peer " << remote_address_; + << "Ignored " << count << "stale " << messageType; break; case ListDisposition::untrusted: JLOG(p_journal_.warn()) - << "Ignored " << count << " untrusted " << messageType - << "(s) from peer " << remote_address_; + << "Ignored " << count << " untrusted " << messageType; break; case ListDisposition::unsupported_version: JLOG(p_journal_.warn()) << "Ignored " << count << "unsupported version " - << messageType << "(s) from peer " << remote_address_; + << messageType; break; case ListDisposition::invalid: JLOG(p_journal_.warn()) - << "Ignored " << count << "invalid " << messageType - << "(s) from peer " << remote_address_; + << "Ignored " << count << "invalid " << messageType; break; // LCOV_EXCL_START default: @@ -2374,8 +2360,7 @@ PeerImp::onMessage(std::shared_ptr const& m) } catch (std::exception const& e) { - JLOG(p_journal_.warn()) << "ValidatorList: Exception, " << e.what() - << " from peer " << remote_address_; + JLOG(p_journal_.warn()) << "ValidatorList: Exception, " << e.what(); using namespace std::string_literals; fee_.update(Resource::feeInvalidData, e.what()); } @@ -2414,8 +2399,8 @@ PeerImp::onMessage( } catch (std::exception const& e) { - JLOG(p_journal_.warn()) << "ValidatorListCollection: Exception, " - << e.what() << " from peer " << remote_address_; + JLOG(p_journal_.warn()) + << "ValidatorListCollection: Exception, " << e.what(); using namespace std::string_literals; fee_.update(Resource::feeInvalidData, e.what()); } diff --git a/src/xrpld/overlay/detail/PeerImp.h b/src/xrpld/overlay/detail/PeerImp.h index c2221c136d..5ce1ece950 100644 --- a/src/xrpld/overlay/detail/PeerImp.h +++ b/src/xrpld/overlay/detail/PeerImp.h @@ -134,6 +134,8 @@ private: Application& app_; id_t const id_; + std::string fingerprint_; + std::string prefix_; beast::WrappedSink sink_; beast::WrappedSink p_sink_; beast::Journal const journal_; @@ -635,7 +637,7 @@ private: cancelTimer() noexcept; static std::string - makePrefix(id_t id); + makePrefix(std::string const& fingerprint); void doAccept(); @@ -689,6 +691,18 @@ private: handleHaveTransactions( std::shared_ptr const& m); + std::string const& + fingerprint() const override + { + return fingerprint_; + } + + std::string const& + prefix() const + { + return prefix_; + } + public: //-------------------------------------------------------------------------- // @@ -832,8 +846,11 @@ PeerImp::PeerImp( : Child(overlay) , app_(app) , id_(id) - , sink_(app_.journal("Peer"), makePrefix(id)) - , p_sink_(app_.journal("Protocol"), makePrefix(id)) + , fingerprint_( + getFingerprint(slot->remote_endpoint(), publicKey, to_string(id_))) + , prefix_(makePrefix(fingerprint_)) + , sink_(app_.journal("Peer"), prefix_) + , p_sink_(app_.journal("Protocol"), prefix_) , journal_(sink_) , p_journal_(p_sink_) , stream_ptr_(std::move(stream_ptr)) diff --git a/src/xrpld/peerfinder/detail/Logic.h b/src/xrpld/peerfinder/detail/Logic.h index 74bec8431e..6a353b1f9a 100644 --- a/src/xrpld/peerfinder/detail/Logic.h +++ b/src/xrpld/peerfinder/detail/Logic.h @@ -35,6 +35,7 @@ #include #include #include +#include #include #include @@ -240,21 +241,23 @@ public: slot.checked = true; slot.connectivityCheckInProgress = false; + beast::WrappedSink sink{m_journal.sink(), slot.prefix()}; + beast::Journal journal{sink}; + if (ec) { // VFALCO TODO Should we retry depending on the error? slot.canAccept = false; - JLOG(m_journal.error()) - << beast::leftw(18) << "Logic testing " << iter->first - << " with error, " << ec.message(); + JLOG(journal.error()) << "Logic testing " << iter->first + << " with error, " << ec.message(); bootcache_.on_failure(checkedAddress); return; } slot.canAccept = true; slot.set_listening_port(checkedAddress.port()); - JLOG(m_journal.debug()) << beast::leftw(18) << "Logic testing " - << checkedAddress << " succeeded"; + JLOG(journal.debug()) + << "Logic testing " << checkedAddress << " succeeded"; } //-------------------------------------------------------------------------- @@ -359,9 +362,10 @@ public: SlotImp::ptr const& slot, beast::IP::Endpoint const& local_endpoint) { - JLOG(m_journal.trace()) - << beast::leftw(18) << "Logic connected" << slot->remote_endpoint() - << " on local " << local_endpoint; + beast::WrappedSink sink{m_journal.sink(), slot->prefix()}; + beast::Journal journal{sink}; + + JLOG(journal.trace()) << "Logic connected on local " << local_endpoint; std::lock_guard _(lock_); @@ -381,9 +385,7 @@ public: iter->second->local_endpoint() == slot->remote_endpoint(), "ripple::PeerFinder::Logic::onConnected : local and remote " "endpoints do match"); - JLOG(m_journal.warn()) - << beast::leftw(18) << "Logic dropping " - << slot->remote_endpoint() << " as self connect"; + JLOG(journal.warn()) << "Logic dropping as self connect"; return false; } } @@ -398,9 +400,12 @@ public: Result activate(SlotImp::ptr const& slot, PublicKey const& key, bool reserved) { - JLOG(m_journal.debug()) - << beast::leftw(18) << "Logic handshake " << slot->remote_endpoint() - << " with " << (reserved ? "reserved " : "") << "key " << key; + beast::WrappedSink sink{m_journal.sink(), slot->prefix()}; + beast::Journal journal{sink}; + + JLOG(journal.debug()) + << "Logic handshake " << slot->remote_endpoint() << " with " + << (reserved ? "reserved " : "") << "key " << key; std::lock_guard _(lock_); @@ -462,8 +467,7 @@ public: "missing from fixed_"); iter->second.success(m_clock.now()); - JLOG(m_journal.trace()) << beast::leftw(18) << "Logic fixed " - << slot->remote_endpoint() << " success"; + JLOG(journal.trace()) << "Logic fixed success"; } return Result::success; @@ -681,9 +685,10 @@ public: { SlotImp::ptr const& slot = t.slot(); auto const& list = t.list(); - JLOG(m_journal.trace()) - << beast::leftw(18) << "Logic sending " - << slot->remote_endpoint() << " with " << list.size() + beast::WrappedSink sink{m_journal.sink(), slot->prefix()}; + beast::Journal journal{sink}; + JLOG(journal.trace()) + << "Logic sending " << list.size() << ((list.size() == 1) ? " endpoint" : " endpoints"); result.push_back(std::make_pair(slot, list)); } @@ -788,6 +793,9 @@ public: void on_endpoints(SlotImp::ptr const& slot, Endpoints list) { + beast::WrappedSink sink{m_journal.sink(), slot->prefix()}; + beast::Journal journal{sink}; + // If we're sent too many endpoints, sample them at random: if (list.size() > Tuning::numberOfEndpointsMax) { @@ -795,10 +803,8 @@ public: list.resize(Tuning::numberOfEndpointsMax); } - JLOG(m_journal.trace()) - << beast::leftw(18) << "Endpoints from " << slot->remote_endpoint() - << " contained " << list.size() - << ((list.size() > 1) ? " entries" : " entry"); + JLOG(journal.trace()) << "Endpoints contained " << list.size() + << ((list.size() > 1) ? " entries" : " entry"); std::lock_guard _(lock_); @@ -835,9 +841,8 @@ public: { if (slot->connectivityCheckInProgress) { - JLOG(m_journal.debug()) - << beast::leftw(18) << "Logic testing " << ep.address - << " already in progress"; + JLOG(journal.debug()) << "Logic testing " << ep.address + << " already in progress"; continue; } @@ -934,6 +939,9 @@ public: remove(slot); + beast::WrappedSink sink{m_journal.sink(), slot->prefix()}; + beast::Journal journal{sink}; + // Mark fixed slot failure if (slot->fixed() && !slot->inbound() && slot->state() != Slot::active) { @@ -944,16 +952,14 @@ public: "missing from fixed_"); iter->second.failure(m_clock.now()); - JLOG(m_journal.debug()) << beast::leftw(18) << "Logic fixed " - << slot->remote_endpoint() << " failed"; + JLOG(journal.debug()) << "Logic fixed failed"; } // Do state specific bookkeeping switch (slot->state()) { case Slot::accept: - JLOG(m_journal.trace()) << beast::leftw(18) << "Logic accept " - << slot->remote_endpoint() << " failed"; + JLOG(journal.trace()) << "Logic accept failed"; break; case Slot::connect: @@ -967,13 +973,11 @@ public: break; case Slot::active: - JLOG(m_journal.trace()) << beast::leftw(18) << "Logic close " - << slot->remote_endpoint(); + JLOG(journal.trace()) << "Logic close"; break; case Slot::closing: - JLOG(m_journal.trace()) << beast::leftw(18) << "Logic finished " - << slot->remote_endpoint(); + JLOG(journal.trace()) << "Logic finished"; break; // LCOV_EXCL_START diff --git a/src/xrpld/peerfinder/detail/SlotImp.h b/src/xrpld/peerfinder/detail/SlotImp.h index e5d8b1e6c5..363fe018e4 100644 --- a/src/xrpld/peerfinder/detail/SlotImp.h +++ b/src/xrpld/peerfinder/detail/SlotImp.h @@ -91,6 +91,12 @@ public: return m_public_key; } + std::string + prefix() const + { + return "[" + getFingerprint(remote_endpoint(), public_key()) + "] "; + } + std::optional listening_port() const override { From bd3bc917f84779a73ba78ead283a25456551c92f Mon Sep 17 00:00:00 2001 From: Pratik Mankawde Date: Wed, 29 Oct 2025 14:21:50 +0000 Subject: [PATCH 17/28] refactor: Retire fix1571 amendment (#5925) Amendments activated for more than 2 years can be retired. This change retires the fix1571 amendment. Co-authored-by: Bart Thomee <11445373+bthomee@users.noreply.github.com> --- include/xrpl/protocol/detail/features.macro | 2 +- src/test/app/Escrow_test.cpp | 99 +++++++--------- src/xrpld/app/tx/detail/Escrow.cpp | 120 ++++---------------- 3 files changed, 64 insertions(+), 157 deletions(-) diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 6670b5fb0b..0aba334de6 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -109,7 +109,6 @@ XRPL_FEATURE(MultiSignReserve, Supported::yes, VoteBehavior::DefaultYe XRPL_FIX (1578, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(DepositPreauth, Supported::yes, VoteBehavior::DefaultYes) XRPL_FIX (1623, Supported::yes, VoteBehavior::DefaultYes) -XRPL_FIX (1571, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(Checks, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(DepositAuth, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(Flow, Supported::yes, VoteBehavior::DefaultYes) @@ -154,3 +153,4 @@ XRPL_RETIRE(fix1513) XRPL_RETIRE(fix1515) XRPL_RETIRE(fix1543) XRPL_RETIRE(fix1781) +XRPL_RETIRE(fix1571) diff --git a/src/test/app/Escrow_test.cpp b/src/test/app/Escrow_test.cpp index cea3a835a6..8d993f9162 100644 --- a/src/test/app/Escrow_test.cpp +++ b/src/test/app/Escrow_test.cpp @@ -294,74 +294,51 @@ struct Escrow_test : public beast::unit_test::suite } void - test1571(FeatureBitset features) + testRequiresConditionOrFinishAfter(FeatureBitset features) { using namespace jtx; using namespace std::chrono; - { - testcase("Implied Finish Time (without fix1571)"); + testcase("RequiresConditionOrFinishAfter"); - Env env(*this, testable_amendments() - fix1571); - auto const baseFee = env.current()->fees().base; - env.fund(XRP(5000), "alice", "bob", "carol"); - env.close(); + Env env(*this, features); + auto const baseFee = env.current()->fees().base; + env.fund(XRP(5000), "alice", "bob", "carol"); + env.close(); - // Creating an escrow without a finish time and finishing it - // is allowed without fix1571: - auto const seq1 = env.seq("alice"); - env(escrow::create("alice", "bob", XRP(100)), - escrow::cancel_time(env.now() + 1s), - fee(baseFee * 150)); - env.close(); - env(escrow::finish("carol", "alice", seq1), fee(baseFee * 150)); - BEAST_EXPECT(env.balance("bob") == XRP(5100)); + // Creating an escrow with only a cancel time is not allowed: + env(escrow::create("alice", "bob", XRP(100)), + escrow::cancel_time(env.now() + 90s), + fee(baseFee * 150), + ter(temMALFORMED)); - env.close(); + // Creating an escrow with only a cancel time and a condition is + // allowed: + auto const seq = env.seq("alice"); + env(escrow::create("alice", "bob", XRP(100)), + escrow::cancel_time(env.now() + 90s), + escrow::condition(escrow::cb1), + fee(baseFee * 150)); + env.close(); + env(escrow::finish("carol", "alice", seq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + BEAST_EXPECT(env.balance("bob") == XRP(5100)); - // Creating an escrow without a finish time and a condition is - // also allowed without fix1571: - auto const seq2 = env.seq("alice"); - env(escrow::create("alice", "bob", XRP(100)), - escrow::cancel_time(env.now() + 1s), - escrow::condition(escrow::cb1), - fee(baseFee * 150)); - env.close(); - env(escrow::finish("carol", "alice", seq2), - escrow::condition(escrow::cb1), - escrow::fulfillment(escrow::fb1), - fee(baseFee * 150)); - BEAST_EXPECT(env.balance("bob") == XRP(5200)); - } - - { - testcase("Implied Finish Time (with fix1571)"); - - 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::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::create("alice", "bob", XRP(100)), - escrow::cancel_time(env.now() + 90s), - escrow::condition(escrow::cb1), - fee(baseFee * 150)); - env.close(); - env(escrow::finish("carol", "alice", seq), - escrow::condition(escrow::cb1), - escrow::fulfillment(escrow::fb1), - fee(baseFee * 150)); - BEAST_EXPECT(env.balance("bob") == XRP(5100)); - } + // Creating an escrow with only a cancel time and a finish time is + // allowed: + auto const seqFt = env.seq("alice"); + env(escrow::create("alice", "bob", XRP(100)), + escrow::finish_time(env.now()), // Set finish time to now so that + // we can call finish immediately. + escrow::cancel_time(env.now() + 50s), + fee(baseFee * 150)); + env.close(); + env(escrow::finish("carol", "alice", seqFt), fee(150 * baseFee)); + BEAST_EXPECT( + env.balance("bob") == + XRP(5200)); // 5100 (from last transaction) + 100 } void @@ -1708,7 +1685,7 @@ struct Escrow_test : public beast::unit_test::suite testTiming(features); testTags(features); testDisallowXRP(features); - test1571(features); + testRequiresConditionOrFinishAfter(features); testFails(features); testLockup(features); testEscrowConditions(features); diff --git a/src/xrpld/app/tx/detail/Escrow.cpp b/src/xrpld/app/tx/detail/Escrow.cpp index a5c44b1e2e..c73ef3f7a1 100644 --- a/src/xrpld/app/tx/detail/Escrow.cpp +++ b/src/xrpld/app/tx/detail/Escrow.cpp @@ -151,15 +151,12 @@ EscrowCreate::preflight(PreflightContext const& ctx) ctx.tx[sfCancelAfter] <= ctx.tx[sfFinishAfter]) return temBAD_EXPIRATION; - if (ctx.rules.enabled(fix1571)) - { - // In the absence of a FinishAfter, the escrow can be finished - // immediately, which can be confusing. When creating an escrow, - // we want to ensure that either a FinishAfter time is explicitly - // specified or a completion condition is attached. - if (!ctx.tx[~sfFinishAfter] && !ctx.tx[~sfCondition]) - return temMALFORMED; - } + // In the absence of a FinishAfter, the escrow can be finished + // immediately, which can be confusing. When creating an escrow, + // we want to ensure that either a FinishAfter time is explicitly + // specified or a completion condition is attached. + if (!ctx.tx[~sfFinishAfter] && !ctx.tx[~sfCondition]) + return temMALFORMED; if (auto const cb = ctx.tx[~sfCondition]) { @@ -446,41 +443,11 @@ EscrowCreate::doApply() { auto const closeTime = ctx_.view().info().parentCloseTime; - // Prior to fix1571, the cancel and finish times could be greater - // than or equal to the parent ledgers' close time. - // - // With fix1571, we require that they both be strictly greater - // than the parent ledgers' close time. - if (ctx_.view().rules().enabled(fix1571)) - { - if (ctx_.tx[~sfCancelAfter] && after(closeTime, ctx_.tx[sfCancelAfter])) - return tecNO_PERMISSION; + if (ctx_.tx[~sfCancelAfter] && after(closeTime, ctx_.tx[sfCancelAfter])) + return tecNO_PERMISSION; - if (ctx_.tx[~sfFinishAfter] && after(closeTime, ctx_.tx[sfFinishAfter])) - return tecNO_PERMISSION; - } - else - { - // This is old code that needs to stay to support replaying old ledgers, - // but does not need to be covered by new tests. - // LCOV_EXCL_START - if (ctx_.tx[~sfCancelAfter]) - { - auto const cancelAfter = ctx_.tx[sfCancelAfter]; - - if (closeTime.time_since_epoch().count() >= cancelAfter) - return tecNO_PERMISSION; - } - - if (ctx_.tx[~sfFinishAfter]) - { - auto const finishAfter = ctx_.tx[sfFinishAfter]; - - if (closeTime.time_since_epoch().count() >= finishAfter) - return tecNO_PERMISSION; - } - // LCOV_EXCL_STOP - } + if (ctx_.tx[~sfFinishAfter] && after(closeTime, ctx_.tx[sfFinishAfter])) + return tecNO_PERMISSION; auto const sle = ctx_.view().peek(keylet::account(account_)); if (!sle) @@ -1027,38 +994,16 @@ EscrowFinish::doApply() } // 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 - // a finish only succeed strictly after the cancel time. - if (ctx_.view().rules().enabled(fix1571)) - { - auto const now = ctx_.view().info().parentCloseTime; + // to that time. + auto const now = ctx_.view().info().parentCloseTime; - // Too soon: can't execute before the finish time - if ((*slep)[~sfFinishAfter] && !after(now, (*slep)[sfFinishAfter])) - return tecNO_PERMISSION; + // Too soon: can't execute before the finish time + if ((*slep)[~sfFinishAfter] && !after(now, (*slep)[sfFinishAfter])) + return tecNO_PERMISSION; - // Too late: can't execute after the cancel time - if ((*slep)[~sfCancelAfter] && after(now, (*slep)[sfCancelAfter])) - return tecNO_PERMISSION; - } - else - { - // This is old code that needs to stay to support replaying old ledgers, - // but does not need to be covered by new tests. - // LCOV_EXCL_START - // Too soon? - if ((*slep)[~sfFinishAfter] && - ctx_.view().info().parentCloseTime.time_since_epoch().count() <= - (*slep)[sfFinishAfter]) - return tecNO_PERMISSION; - - // Too late? - if ((*slep)[~sfCancelAfter] && - ctx_.view().info().parentCloseTime.time_since_epoch().count() <= - (*slep)[sfCancelAfter]) - return tecNO_PERMISSION; - // LCOV_EXCL_STOP - } + // Too late: can't execute after the cancel time + if ((*slep)[~sfCancelAfter] && after(now, (*slep)[sfCancelAfter])) + return tecNO_PERMISSION; // Check cryptocondition fulfillment { @@ -1315,30 +1260,15 @@ EscrowCancel::doApply() return tecNO_TARGET; } - if (ctx_.view().rules().enabled(fix1571)) - { - auto const now = ctx_.view().info().parentCloseTime; + auto const now = ctx_.view().info().parentCloseTime; - // No cancel time specified: can't execute at all. - if (!(*slep)[~sfCancelAfter]) - return tecNO_PERMISSION; + // No cancel time specified: can't execute at all. + if (!(*slep)[~sfCancelAfter]) + return tecNO_PERMISSION; - // Too soon: can't execute before the cancel time. - if (!after(now, (*slep)[sfCancelAfter])) - return tecNO_PERMISSION; - } - else - { - // This is old code that needs to stay to support replaying old ledgers, - // but does not need to be covered by new tests. - // LCOV_EXCL_START - // Too soon? - if (!(*slep)[~sfCancelAfter] || - ctx_.view().info().parentCloseTime.time_since_epoch().count() <= - (*slep)[sfCancelAfter]) - return tecNO_PERMISSION; - // LCOV_EXCL_STOP - } + // Too soon: can't execute before the cancel time. + if (!after(now, (*slep)[sfCancelAfter])) + return tecNO_PERMISSION; AccountID const account = (*slep)[sfAccount]; From efa917d9f32958ab9ebb2f0c284657a96901e922 Mon Sep 17 00:00:00 2001 From: Pratik Mankawde Date: Wed, 29 Oct 2025 16:08:17 +0000 Subject: [PATCH 18/28] refactor: Retire fix1578 amendment (#5927) Amendments activated for more than 2 years can be retired. This change retires the fix1578 amendment. --- include/xrpl/protocol/detail/features.macro | 2 +- src/test/app/AMMExtended_test.cpp | 188 +++++++++----------- src/test/app/Offer_test.cpp | 14 +- src/test/rpc/Feature_test.cpp | 1 - src/test/rpc/NoRipple_test.cpp | 135 +++++++------- src/xrpld/app/tx/detail/CreateOffer.cpp | 4 +- src/xrpld/app/tx/detail/SetTrust.cpp | 2 +- 7 files changed, 158 insertions(+), 188 deletions(-) diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 0aba334de6..6d3da84f84 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -106,7 +106,6 @@ XRPL_FIX (CheckThreading, Supported::yes, VoteBehavior::DefaultYe XRPL_FIX (MasterKeyAsRegularKey, Supported::yes, VoteBehavior::DefaultYes) XRPL_FIX (TakerDryOfferRemoval, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(MultiSignReserve, Supported::yes, VoteBehavior::DefaultYes) -XRPL_FIX (1578, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(DepositPreauth, Supported::yes, VoteBehavior::DefaultYes) XRPL_FIX (1623, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(Checks, Supported::yes, VoteBehavior::DefaultYes) @@ -154,3 +153,4 @@ XRPL_RETIRE(fix1515) XRPL_RETIRE(fix1543) XRPL_RETIRE(fix1781) XRPL_RETIRE(fix1571) +XRPL_RETIRE(fix1578) diff --git a/src/test/app/AMMExtended_test.cpp b/src/test/app/AMMExtended_test.cpp index ee6988bd75..b0e810fce3 100644 --- a/src/test/app/AMMExtended_test.cpp +++ b/src/test/app/AMMExtended_test.cpp @@ -198,109 +198,100 @@ private: // Fill or Kill - unless we fully cross, just charge a fee and don't // place the offer on the books. But also clean up expired offers // that are discovered along the way. - // - // fix1578 changes the return code. Verify expected behavior - // without and with fix1578. - for (auto const& tweakedFeatures : - {features - fix1578, features | fix1578}) - { - testAMM( - [&](AMM& ammAlice, Env& env) { - // Order that can't be filled - TER const killedCode{ - tweakedFeatures[fix1578] ? TER{tecKILLED} - : TER{tesSUCCESS}}; - env(offer(carol, USD(100), XRP(100)), - txflags(tfFillOrKill), - ter(killedCode)); - env.close(); - BEAST_EXPECT(ammAlice.expectBalances( - XRP(10'100), USD(10'000), ammAlice.tokens())); - // fee = AMM - BEAST_EXPECT(expectLedgerEntryRoot( - env, carol, XRP(30'000) - (txfee(env, 1)))); - BEAST_EXPECT(expectOffers(env, carol, 0)); - BEAST_EXPECT(expectHolding(env, carol, USD(30'000))); + testAMM( + [&](AMM& ammAlice, Env& env) { + // Order that can't be filled + TER const killedCode{TER{tecKILLED}}; + env(offer(carol, USD(100), XRP(100)), + txflags(tfFillOrKill), + ter(killedCode)); + env.close(); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10'100), USD(10'000), ammAlice.tokens())); + // fee = AMM + BEAST_EXPECT(expectLedgerEntryRoot( + env, carol, XRP(30'000) - (txfee(env, 1)))); + BEAST_EXPECT(expectOffers(env, carol, 0)); + BEAST_EXPECT(expectHolding(env, carol, USD(30'000))); - // Order that can be filled - env(offer(carol, XRP(100), USD(100)), - txflags(tfFillOrKill), - ter(tesSUCCESS)); - BEAST_EXPECT(ammAlice.expectBalances( - XRP(10'000), USD(10'100), ammAlice.tokens())); - BEAST_EXPECT(expectLedgerEntryRoot( - env, carol, XRP(30'000) + XRP(100) - txfee(env, 2))); - BEAST_EXPECT(expectHolding(env, carol, USD(29'900))); - BEAST_EXPECT(expectOffers(env, carol, 0)); - }, - {{XRP(10'100), USD(10'000)}}, - 0, - std::nullopt, - {tweakedFeatures}); + // Order that can be filled + env(offer(carol, XRP(100), USD(100)), + txflags(tfFillOrKill), + ter(tesSUCCESS)); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10'000), USD(10'100), ammAlice.tokens())); + BEAST_EXPECT(expectLedgerEntryRoot( + env, carol, XRP(30'000) + XRP(100) - txfee(env, 2))); + BEAST_EXPECT(expectHolding(env, carol, USD(29'900))); + BEAST_EXPECT(expectOffers(env, carol, 0)); + }, + {{XRP(10'100), USD(10'000)}}, + 0, + std::nullopt, + {features}); - // Immediate or Cancel - cross as much as possible - // and add nothing on the books. - testAMM( - [&](AMM& ammAlice, Env& env) { - env(offer(carol, XRP(200), USD(200)), - txflags(tfImmediateOrCancel), - ter(tesSUCCESS)); + // Immediate or Cancel - cross as much as possible + // and add nothing on the books. + testAMM( + [&](AMM& ammAlice, Env& env) { + env(offer(carol, XRP(200), USD(200)), + txflags(tfImmediateOrCancel), + ter(tesSUCCESS)); - // AMM generates a synthetic offer of 100USD/100XRP - // to match the CLOB offer quality. - BEAST_EXPECT(ammAlice.expectBalances( - XRP(10'000), USD(10'100), ammAlice.tokens())); - // +AMM - offer * fee - BEAST_EXPECT(expectLedgerEntryRoot( - env, carol, XRP(30'000) + XRP(100) - txfee(env, 1))); - // AMM - BEAST_EXPECT(expectHolding(env, carol, USD(29'900))); - BEAST_EXPECT(expectOffers(env, carol, 0)); - }, - {{XRP(10'100), USD(10'000)}}, - 0, - std::nullopt, - {tweakedFeatures}); + // AMM generates a synthetic offer of 100USD/100XRP + // to match the CLOB offer quality. + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10'000), USD(10'100), ammAlice.tokens())); + // +AMM - offer * fee + BEAST_EXPECT(expectLedgerEntryRoot( + env, carol, XRP(30'000) + XRP(100) - txfee(env, 1))); + // AMM + BEAST_EXPECT(expectHolding(env, carol, USD(29'900))); + BEAST_EXPECT(expectOffers(env, carol, 0)); + }, + {{XRP(10'100), USD(10'000)}}, + 0, + std::nullopt, + {features}); - // tfPassive -- place the offer without crossing it. - testAMM( - [&](AMM& ammAlice, Env& env) { - // Carol creates a passive offer that could cross AMM. - // Carol's offer should stay in the ledger. - env(offer(carol, XRP(100), USD(100), tfPassive)); - env.close(); - BEAST_EXPECT(ammAlice.expectBalances( - XRP(10'100), STAmount{USD, 10'000}, ammAlice.tokens())); - BEAST_EXPECT(expectOffers( - env, carol, 1, {{{XRP(100), STAmount{USD, 100}}}})); - }, - {{XRP(10'100), USD(10'000)}}, - 0, - std::nullopt, - {tweakedFeatures}); + // tfPassive -- place the offer without crossing it. + testAMM( + [&](AMM& ammAlice, Env& env) { + // Carol creates a passive offer that could cross AMM. + // Carol's offer should stay in the ledger. + env(offer(carol, XRP(100), USD(100), tfPassive)); + env.close(); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10'100), STAmount{USD, 10'000}, ammAlice.tokens())); + BEAST_EXPECT(expectOffers( + env, carol, 1, {{{XRP(100), STAmount{USD, 100}}}})); + }, + {{XRP(10'100), USD(10'000)}}, + 0, + std::nullopt, + {features}); - // tfPassive -- cross only offers of better quality. - testAMM( - [&](AMM& ammAlice, Env& env) { - env(offer(alice, USD(110), XRP(100))); - env.close(); + // tfPassive -- cross only offers of better quality. + testAMM( + [&](AMM& ammAlice, Env& env) { + env(offer(alice, USD(110), XRP(100))); + env.close(); - // Carol creates a passive offer. That offer should cross - // AMM and leave Alice's offer untouched. - env(offer(carol, XRP(100), USD(100), tfPassive)); - env.close(); - BEAST_EXPECT(ammAlice.expectBalances( - XRP(10'900), - STAmount{USD, UINT64_C(9'082'56880733945), -11}, - ammAlice.tokens())); - BEAST_EXPECT(expectOffers(env, carol, 0)); - BEAST_EXPECT(expectOffers(env, alice, 1)); - }, - {{XRP(11'000), USD(9'000)}}, - 0, - std::nullopt, - {tweakedFeatures}); - } + // Carol creates a passive offer. That offer should cross + // AMM and leave Alice's offer untouched. + env(offer(carol, XRP(100), USD(100), tfPassive)); + env.close(); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10'900), + STAmount{USD, UINT64_C(9'082'56880733945), -11}, + ammAlice.tokens())); + BEAST_EXPECT(expectOffers(env, carol, 0)); + BEAST_EXPECT(expectOffers(env, alice, 1)); + }, + {{XRP(11'000), USD(9'000)}}, + 0, + std::nullopt, + {features}); } void @@ -867,8 +858,7 @@ private: using namespace jtx; // Code returned if an offer is killed. - TER const killedCode{ - features[fix1578] ? TER{tecKILLED} : TER{tesSUCCESS}}; + TER const killedCode{TER{tecKILLED}}; { Env env{*this, features}; diff --git a/src/test/app/Offer_test.cpp b/src/test/app/Offer_test.cpp index 06dcd6e53c..709d3b213b 100644 --- a/src/test/app/Offer_test.cpp +++ b/src/test/app/Offer_test.cpp @@ -846,13 +846,8 @@ public: // Fill or Kill - unless we fully cross, just charge a fee and don't // place the offer on the books. But also clean up expired offers // that are discovered along the way. - // - // fix1578 changes the return code. Verify expected behavior - // without and with fix1578. - for (auto const& tweakedFeatures : - {features - fix1578, features | fix1578}) { - Env env{*this, tweakedFeatures}; + Env env{*this, features}; auto const f = env.current()->fees().base; @@ -878,9 +873,7 @@ public: // Order that can't be filled but will remove bob's expired offer: { - TER const killedCode{ - tweakedFeatures[fix1578] ? TER{tecKILLED} - : TER{tesSUCCESS}}; + TER const killedCode{TER{tecKILLED}}; env(offer(alice, XRP(1000), USD(1000)), txflags(tfFillOrKill), ter(killedCode)); @@ -3008,8 +3001,7 @@ public: env.close(); // Code returned if an offer is killed. - TER const killedCode{ - features[fix1578] ? TER{tecKILLED} : TER{tesSUCCESS}}; + TER const killedCode{TER{tecKILLED}}; // bob offers XRP for USD. env(trust(bob, USD(200))); diff --git a/src/test/rpc/Feature_test.cpp b/src/test/rpc/Feature_test.cpp index 84fc284b13..895988152c 100644 --- a/src/test/rpc/Feature_test.cpp +++ b/src/test/rpc/Feature_test.cpp @@ -143,7 +143,6 @@ class Feature_test : public beast::unit_test::suite featureToName(fixTrustLinesToSelf) == "fixTrustLinesToSelf"); BEAST_EXPECT(featureToName(featureFlow) == "Flow"); BEAST_EXPECT(featureToName(featureNegativeUNL) == "NegativeUNL"); - BEAST_EXPECT(featureToName(fix1578) == "fix1578"); BEAST_EXPECT( featureToName(fixTakerDryOfferRemoval) == "fixTakerDryOfferRemoval"); diff --git a/src/test/rpc/NoRipple_test.cpp b/src/test/rpc/NoRipple_test.cpp index 93457ada8c..0f2b4748b6 100644 --- a/src/test/rpc/NoRipple_test.cpp +++ b/src/test/rpc/NoRipple_test.cpp @@ -85,83 +85,74 @@ public: auto const bob = Account("bob"); auto const carol = Account("carol"); - // fix1578 changes the return code. Verify expected behavior - // without and with fix1578. - for (auto const& tweakedFeatures : - {features - fix1578, features | fix1578}) + Env env(*this, features); + + env.fund(XRP(10000), gw, alice, bob, carol); + env.close(); + + env.trust(alice["USD"](100), bob); + env.trust(bob["USD"](100), carol); + env.close(); + + // After this payment alice has a -50 USD balance with bob, and + // bob has a -50 USD balance with carol. So neither alice nor + // bob should be able to clear the noRipple flag. + env(pay(alice, carol, carol["USD"](50)), path(bob)); + env.close(); + + TER const terNeg{TER{tecNO_PERMISSION}}; + + env(trust(alice, bob["USD"](100), bob, tfSetNoRipple), ter(terNeg)); + env(trust(bob, carol["USD"](100), carol, tfSetNoRipple), ter(terNeg)); + env.close(); + + Json::Value params; + params[jss::source_account] = alice.human(); + params[jss::destination_account] = carol.human(); + params[jss::destination_amount] = [] { + Json::Value dest_amt; + dest_amt[jss::currency] = "USD"; + dest_amt[jss::value] = "1"; + dest_amt[jss::issuer] = Account("carol").human(); + return dest_amt; + }(); + + auto const resp = + env.rpc("json", "ripple_path_find", to_string(params)); + BEAST_EXPECT(resp[jss::result][jss::alternatives].size() == 1); + + auto getAccountLines = [&env](Account const& acct) { + auto const r = jtx::getAccountLines(env, acct); + return r[jss::lines]; + }; { - Env env(*this, tweakedFeatures); + auto const aliceLines = getAccountLines(alice); + BEAST_EXPECT(aliceLines.size() == 1); + BEAST_EXPECT(aliceLines[0u][jss::no_ripple].asBool() == false); - env.fund(XRP(10000), gw, alice, bob, carol); - env.close(); + auto const bobLines = getAccountLines(bob); + BEAST_EXPECT(bobLines.size() == 2); + BEAST_EXPECT(bobLines[0u][jss::no_ripple].asBool() == false); + BEAST_EXPECT(bobLines[1u][jss::no_ripple].asBool() == false); + } - env.trust(alice["USD"](100), bob); - env.trust(bob["USD"](100), carol); - env.close(); + // Now carol sends the 50 USD back to alice. Then alice and + // bob can set the noRipple flag. + env(pay(carol, alice, alice["USD"](50)), path(bob)); + env.close(); - // After this payment alice has a -50 USD balance with bob, and - // bob has a -50 USD balance with carol. So neither alice nor - // bob should be able to clear the noRipple flag. - env(pay(alice, carol, carol["USD"](50)), path(bob)); - env.close(); + env(trust(alice, bob["USD"](100), bob, tfSetNoRipple)); + env(trust(bob, carol["USD"](100), carol, tfSetNoRipple)); + env.close(); + { + auto const aliceLines = getAccountLines(alice); + BEAST_EXPECT(aliceLines.size() == 1); + BEAST_EXPECT(aliceLines[0u].isMember(jss::no_ripple)); - TER const terNeg{ - tweakedFeatures[fix1578] ? TER{tecNO_PERMISSION} - : TER{tesSUCCESS}}; - - env(trust(alice, bob["USD"](100), bob, tfSetNoRipple), ter(terNeg)); - env(trust(bob, carol["USD"](100), carol, tfSetNoRipple), - ter(terNeg)); - env.close(); - - Json::Value params; - params[jss::source_account] = alice.human(); - params[jss::destination_account] = carol.human(); - params[jss::destination_amount] = [] { - Json::Value dest_amt; - dest_amt[jss::currency] = "USD"; - dest_amt[jss::value] = "1"; - dest_amt[jss::issuer] = Account("carol").human(); - return dest_amt; - }(); - - auto const resp = - env.rpc("json", "ripple_path_find", to_string(params)); - BEAST_EXPECT(resp[jss::result][jss::alternatives].size() == 1); - - auto getAccountLines = [&env](Account const& acct) { - auto const r = jtx::getAccountLines(env, acct); - return r[jss::lines]; - }; - { - auto const aliceLines = getAccountLines(alice); - BEAST_EXPECT(aliceLines.size() == 1); - BEAST_EXPECT(aliceLines[0u][jss::no_ripple].asBool() == false); - - auto const bobLines = getAccountLines(bob); - BEAST_EXPECT(bobLines.size() == 2); - BEAST_EXPECT(bobLines[0u][jss::no_ripple].asBool() == false); - BEAST_EXPECT(bobLines[1u][jss::no_ripple].asBool() == false); - } - - // Now carol sends the 50 USD back to alice. Then alice and - // bob can set the noRipple flag. - env(pay(carol, alice, alice["USD"](50)), path(bob)); - env.close(); - - env(trust(alice, bob["USD"](100), bob, tfSetNoRipple)); - env(trust(bob, carol["USD"](100), carol, tfSetNoRipple)); - env.close(); - { - auto const aliceLines = getAccountLines(alice); - BEAST_EXPECT(aliceLines.size() == 1); - BEAST_EXPECT(aliceLines[0u].isMember(jss::no_ripple)); - - auto const bobLines = getAccountLines(bob); - BEAST_EXPECT(bobLines.size() == 2); - BEAST_EXPECT(bobLines[0u].isMember(jss::no_ripple_peer)); - BEAST_EXPECT(bobLines[1u].isMember(jss::no_ripple)); - } + auto const bobLines = getAccountLines(bob); + BEAST_EXPECT(bobLines.size() == 2); + BEAST_EXPECT(bobLines[0u].isMember(jss::no_ripple_peer)); + BEAST_EXPECT(bobLines[1u].isMember(jss::no_ripple)); } } diff --git a/src/xrpld/app/tx/detail/CreateOffer.cpp b/src/xrpld/app/tx/detail/CreateOffer.cpp index a503f913fa..6303918ef2 100644 --- a/src/xrpld/app/tx/detail/CreateOffer.cpp +++ b/src/xrpld/app/tx/detail/CreateOffer.cpp @@ -795,9 +795,7 @@ CreateOffer::applyGuts(Sandbox& sb, Sandbox& sbCancel) if (bFillOrKill) { JLOG(j_.trace()) << "Fill or Kill: offer killed"; - if (sb.rules().enabled(fix1578)) - return {tecKILLED, false}; - return {tesSUCCESS, false}; + return {tecKILLED, false}; } // For 'immediate or cancel' offers, the amount remaining doesn't get diff --git a/src/xrpld/app/tx/detail/SetTrust.cpp b/src/xrpld/app/tx/detail/SetTrust.cpp index d881425960..59333f3077 100644 --- a/src/xrpld/app/tx/detail/SetTrust.cpp +++ b/src/xrpld/app/tx/detail/SetTrust.cpp @@ -576,7 +576,7 @@ SetTrust::doApply() if ((bHigh ? saHighBalance : saLowBalance) >= beast::zero) uFlagsOut |= (bHigh ? lsfHighNoRipple : lsfLowNoRipple); - else if (view().rules().enabled(fix1578)) + else // Cannot set noRipple on a negative balance. return tecNO_PERMISSION; } From 553fb5be3bab1fd1bf0553d4bed9d44191e14bf1 Mon Sep 17 00:00:00 2001 From: Jingchen Date: Wed, 29 Oct 2025 16:36:51 +0000 Subject: [PATCH 19/28] refactor: Retire fixCheckThreading amendment (#5957) Amendments activated for more than 2 years can be retired. This change retires the fixCheckThreading amendment. --- include/xrpl/protocol/detail/features.macro | 2 +- src/libxrpl/ledger/ApplyStateTable.cpp | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 6d3da84f84..aa759603e9 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -102,7 +102,6 @@ XRPL_FEATURE(RequireFullyCanonicalSig, Supported::yes, VoteBehavior::DefaultYe XRPL_FIX (QualityUpperBound, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(DeletableAccounts, Supported::yes, VoteBehavior::DefaultYes) XRPL_FIX (PayChanRecipientOwnerDir, Supported::yes, VoteBehavior::DefaultYes) -XRPL_FIX (CheckThreading, Supported::yes, VoteBehavior::DefaultYes) XRPL_FIX (MasterKeyAsRegularKey, Supported::yes, VoteBehavior::DefaultYes) XRPL_FIX (TakerDryOfferRemoval, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(MultiSignReserve, Supported::yes, VoteBehavior::DefaultYes) @@ -154,3 +153,4 @@ XRPL_RETIRE(fix1543) XRPL_RETIRE(fix1781) XRPL_RETIRE(fix1571) XRPL_RETIRE(fix1578) +XRPL_RETIRE(fixCheckThreading) diff --git a/src/libxrpl/ledger/ApplyStateTable.cpp b/src/libxrpl/ledger/ApplyStateTable.cpp index aaad056c58..99b76e2d11 100644 --- a/src/libxrpl/ledger/ApplyStateTable.cpp +++ b/src/libxrpl/ledger/ApplyStateTable.cpp @@ -682,12 +682,6 @@ ApplyStateTable::threadOwners( if (auto const optSleAcct{(*sle)[~sfAccount]}) threadTx(base, meta, *optSleAcct, mods, j); - // Don't thread a check's sfDestination unless the amendment is - // enabled - if (ledgerType == ltCHECK && - !base.rules().enabled(fixCheckThreading)) - break; - // If sfDestination is present, thread to that account if (auto const optSleDest{(*sle)[~sfDestination]}) threadTx(base, meta, *optSleDest, mods, j); From 48d38c1e2ca1d73612776f3a27eb472847429fa7 Mon Sep 17 00:00:00 2001 From: Bart Date: Wed, 29 Oct 2025 13:03:16 -0400 Subject: [PATCH 20/28] refactor: Sorts retired amendments to reduce conflicts (#5966) We are on an amendment retiring spree, but each change results in conflicts in `features.macro` because currently they all add the retired amendment to the end of the list. By sorting the list the number of conflicts should be reduced, making it easier to merge them. --- include/xrpl/protocol/detail/features.macro | 30 ++++++++++----------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index aa759603e9..d2ff03cab7 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -131,26 +131,26 @@ XRPL_FEATURE(CryptoConditionsSuite, Supported::yes, VoteBehavior::Obsolete) // pre-amendment code has been removed and the identifiers are deprecated. // All known amendments and amendments that may appear in a validated // ledger must be registered either here or above with the "active" amendments -XRPL_RETIRE(MultiSign) -XRPL_RETIRE(TrustSetAuth) -XRPL_RETIRE(FeeEscalation) -XRPL_RETIRE(PayChan) -XRPL_RETIRE(CryptoConditions) -XRPL_RETIRE(TickSize) -XRPL_RETIRE(fix1368) -XRPL_RETIRE(Escrow) -XRPL_RETIRE(fix1373) -XRPL_RETIRE(EnforceInvariants) -XRPL_RETIRE(SortedDirectories) XRPL_RETIRE(fix1201) +XRPL_RETIRE(fix1368) +XRPL_RETIRE(fix1373) XRPL_RETIRE(fix1512) -XRPL_RETIRE(fix1523) -XRPL_RETIRE(fix1528) -XRPL_RETIRE(FlowCross) XRPL_RETIRE(fix1513) XRPL_RETIRE(fix1515) +XRPL_RETIRE(fix1523) +XRPL_RETIRE(fix1528) XRPL_RETIRE(fix1543) -XRPL_RETIRE(fix1781) XRPL_RETIRE(fix1571) XRPL_RETIRE(fix1578) +XRPL_RETIRE(fix1781) XRPL_RETIRE(fixCheckThreading) +XRPL_RETIRE(CryptoConditions) +XRPL_RETIRE(Escrow) +XRPL_RETIRE(EnforceInvariants) +XRPL_RETIRE(FeeEscalation) +XRPL_RETIRE(FlowCross) +XRPL_RETIRE(MultiSign) +XRPL_RETIRE(PayChan) +XRPL_RETIRE(SortedDirectories) +XRPL_RETIRE(TickSize) +XRPL_RETIRE(TrustSetAuth) From 80a3ae6386b257bff2475ac8055b3422be707127 Mon Sep 17 00:00:00 2001 From: Pratik Mankawde Date: Wed, 29 Oct 2025 17:34:06 +0000 Subject: [PATCH 21/28] refactor: Retire fixRmSmallIncreasedQOffers amendment (#5955) Amendments activated for more than 2 years can be retired. This change retires the fixRmSmallIncreasedQOffers amendment. --- include/xrpl/protocol/detail/features.macro | 2 +- src/test/app/Offer_test.cpp | 141 +++++--------------- src/xrpld/app/tx/detail/OfferStream.cpp | 3 - 3 files changed, 38 insertions(+), 108 deletions(-) diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index d2ff03cab7..b80b61fc8c 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -91,7 +91,6 @@ XRPL_FIX (TrustLinesToSelf, Supported::yes, VoteBehavior::DefaultNo XRPL_FEATURE(NonFungibleTokensV1_1, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(ExpandedSignerList, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(CheckCashMakesTrustLine, Supported::yes, VoteBehavior::DefaultNo) -XRPL_FIX (RmSmallIncreasedQOffers, Supported::yes, VoteBehavior::DefaultYes) XRPL_FIX (STAmountCanonicalize, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(FlowSortStrands, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(TicketBatch, Supported::yes, VoteBehavior::DefaultYes) @@ -144,6 +143,7 @@ XRPL_RETIRE(fix1571) XRPL_RETIRE(fix1578) XRPL_RETIRE(fix1781) XRPL_RETIRE(fixCheckThreading) +XRPL_RETIRE(fixRmSmallIncreasedQOffers) XRPL_RETIRE(CryptoConditions) XRPL_RETIRE(Escrow) XRPL_RETIRE(EnforceInvariants) diff --git a/src/test/app/Offer_test.cpp b/src/test/app/Offer_test.cpp index 709d3b213b..c279afd2a2 100644 --- a/src/test/app/Offer_test.cpp +++ b/src/test/app/Offer_test.cpp @@ -366,37 +366,22 @@ public: env(offer(alice, USD(1), aliceTakerGets)); env.close(); - if (features[fixRmSmallIncreasedQOffers]) + env.require( + offers(carol, 0), + balance( + carol, + initialCarolUSD)); // offer is removed but not taken + if (crossBothOffers) { env.require( - offers(carol, 0), - balance( - carol, - initialCarolUSD)); // offer is removed but not taken - if (crossBothOffers) - { - env.require( - offers(alice, 0), - balance(alice, USD(1))); // alice's offer is crossed - } - else - { - env.require( - offers(alice, 1), - balance( - alice, USD(0))); // alice's offer is not crossed - } + offers(alice, 0), + balance(alice, USD(1))); // alice's offer is crossed } else { env.require( offers(alice, 1), - offers(bob, 1), - offers(carol, 1), - balance(alice, USD(0)), - balance( - carol, - initialCarolUSD)); // offer is not crossed at all + balance(alice, USD(0))); // alice's offer is not crossed } } @@ -434,36 +419,19 @@ public: ter(expectedTer)); env.close(); - if (features[fixRmSmallIncreasedQOffers]) + if (expectedTer == tesSUCCESS) { - if (expectedTer == tesSUCCESS) - { - env.require(offers(carol, 0)); - env.require(balance( - carol, - initialCarolUSD)); // offer is removed but not taken - } - else - { - // TODO: Offers are not removed when payments fail - // If that is addressed, the test should show that carol's - // offer is removed but not taken, as in the other branch of - // this if statement - } + env.require(offers(carol, 0)); + env.require(balance( + carol, + initialCarolUSD)); // offer is removed but not taken } else { - if (partialPayment) - { - env.require(offers(carol, 0)); - env.require( - balance(carol, USD(0))); // offer is removed and taken - } - else - { - // offer is not removed or taken - BEAST_EXPECT(isOffer(env, carol, drops(1), USD(1))); - } + // TODO: Offers are not removed when payments fail + // If that is addressed, the test should show that carol's + // offer is removed but not taken, as in the other branch of + // this if statement } } } @@ -526,37 +494,22 @@ public: env(offer(alice, USD(1), aliceTakerGets)); env.close(); - if (features[fixRmSmallIncreasedQOffers]) + env.require( + offers(carol, 0), + balance( + carol, + initialCarolUSD)); // offer is removed but not taken + if (crossBothOffers) { env.require( - offers(carol, 0), - balance( - carol, - initialCarolUSD)); // offer is removed but not taken - if (crossBothOffers) - { - env.require( - offers(alice, 0), - balance(alice, USD(1))); // alice's offer is crossed - } - else - { - env.require( - offers(alice, 1), - balance( - alice, USD(0))); // alice's offer is not crossed - } + offers(alice, 0), + balance(alice, USD(1))); // alice's offer is crossed } else { env.require( offers(alice, 1), - offers(bob, 1), - offers(carol, 1), - balance(alice, USD(0)), - balance( - carol, - initialCarolUSD)); // offer is not crossed at all + balance(alice, USD(0))); // alice's offer is not crossed } } @@ -597,36 +550,19 @@ public: ter(expectedTer)); env.close(); - if (features[fixRmSmallIncreasedQOffers]) + if (expectedTer == tesSUCCESS) { - if (expectedTer == tesSUCCESS) - { - env.require(offers(carol, 0)); - env.require(balance( - carol, - initialCarolUSD)); // offer is removed but not taken - } - else - { - // TODO: Offers are not removed when payments fail - // If that is addressed, the test should show that carol's - // offer is removed but not taken, as in the other branch of - // this if statement - } + env.require(offers(carol, 0)); + env.require(balance( + carol, + initialCarolUSD)); // offer is removed but not taken } else { - if (partialPayment) - { - env.require(offers(carol, 0)); - env.require( - balance(carol, USD(0))); // offer is removed and taken - } - else - { - // offer is not removed or taken - BEAST_EXPECT(isOffer(env, carol, EUR(1), USD(2))); - } + // TODO: Offers are not removed when payments fail + // If that is addressed, the test should show that carol's + // offer is removed but not taken, as in the other branch of + // this if statement } } } @@ -5359,8 +5295,6 @@ public: using namespace jtx; 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}; @@ -5369,8 +5303,7 @@ public: static std::array const feats{ all - takerDryOffer - immediateOfferKilled - permDEX, all - immediateOfferKilled - permDEX, - all - rmSmallIncreasedQOffers - immediateOfferKilled - fillOrKill - - permDEX, + all - immediateOfferKilled - fillOrKill - permDEX, all - fillOrKill - permDEX, all - permDEX, all}; diff --git a/src/xrpld/app/tx/detail/OfferStream.cpp b/src/xrpld/app/tx/detail/OfferStream.cpp index 9d43e419d7..8f19c20cf5 100644 --- a/src/xrpld/app/tx/detail/OfferStream.cpp +++ b/src/xrpld/app/tx/detail/OfferStream.cpp @@ -158,9 +158,6 @@ TOfferStreamBase::shouldRmSmallIncreasedQOffer() const !std::is_same_v, "Cannot have XRP/XRP offers"); - if (!view_.rules().enabled(fixRmSmallIncreasedQOffers)) - return false; - // Consider removing the offer if: // o `TakerPays` is XRP (because of XRP drops granularity) or // o `TakerPays` and `TakerGets` are both IOU and `TakerPays`<`TakerGets` From f8b4f692f1cb3b971138b71912bcd2c2eb9ee4d5 Mon Sep 17 00:00:00 2001 From: Pratik Mankawde Date: Wed, 29 Oct 2025 18:17:50 +0000 Subject: [PATCH 22/28] refactor: Retire fixSTAmountCanonicalize code (#5956) Amendments activated for more than 2 years can be retired. This change retires the fixSTAmountCanonicalize amendment. --- include/xrpl/protocol/STAmount.h | 31 ----------- include/xrpl/protocol/detail/features.macro | 2 +- src/libxrpl/protocol/STAmount.cpp | 59 ++++++--------------- src/xrpld/app/misc/detail/TxQ.cpp | 2 - src/xrpld/app/tx/detail/Transactor.cpp | 5 +- src/xrpld/app/tx/detail/apply.cpp | 2 - 6 files changed, 18 insertions(+), 83 deletions(-) diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index f1e34463b6..ee68e33000 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -709,37 +709,6 @@ 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 -// to unconditionally use the new behavior. - -bool -getSTAmountCanonicalizeSwitchover(); - -void -setSTAmountCanonicalizeSwitchover(bool v); - -/** RAII class to set and restore the STAmount canonicalize switchover. - */ - -class STAmountSO -{ -public: - explicit STAmountSO(bool v) : saved_(getSTAmountCanonicalizeSwitchover()) - { - setSTAmountCanonicalizeSwitchover(v); - } - - ~STAmountSO() - { - setSTAmountCanonicalizeSwitchover(saved_); - } - -private: - bool saved_; -}; - } // namespace ripple //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index b80b61fc8c..0c817829db 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -91,7 +91,6 @@ XRPL_FIX (TrustLinesToSelf, Supported::yes, VoteBehavior::DefaultNo XRPL_FEATURE(NonFungibleTokensV1_1, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(ExpandedSignerList, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(CheckCashMakesTrustLine, Supported::yes, VoteBehavior::DefaultNo) -XRPL_FIX (STAmountCanonicalize, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(FlowSortStrands, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(TicketBatch, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(NegativeUNL, Supported::yes, VoteBehavior::DefaultYes) @@ -144,6 +143,7 @@ XRPL_RETIRE(fix1578) XRPL_RETIRE(fix1781) XRPL_RETIRE(fixCheckThreading) XRPL_RETIRE(fixRmSmallIncreasedQOffers) +XRPL_RETIRE(fixSTAmountCanonicalize) XRPL_RETIRE(CryptoConditions) XRPL_RETIRE(Escrow) XRPL_RETIRE(EnforceInvariants) diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 0c72244885..dfd1b3dfbe 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -68,29 +68,6 @@ namespace ripple { -namespace { - -// Use a static inside a function to help prevent order-of-initialzation issues -LocalValue& -getStaticSTAmountCanonicalizeSwitchover() -{ - static LocalValue r{true}; - return r; -} -} // namespace - -bool -getSTAmountCanonicalizeSwitchover() -{ - return *getStaticSTAmountCanonicalizeSwitchover(); -} - -void -setSTAmountCanonicalizeSwitchover(bool v) -{ - *getStaticSTAmountCanonicalizeSwitchover() = v; -} - static std::uint64_t const tenTo14 = 100000000000000ull; static std::uint64_t const tenTo14m1 = tenTo14 - 1; static std::uint64_t const tenTo17 = tenTo14 * 1000; @@ -884,18 +861,14 @@ STAmount::canonicalize() return; } - if (getSTAmountCanonicalizeSwitchover()) - { - // log(cMaxNativeN, 10) == 17 - if (native() && mOffset > 17) - Throw( - "Native currency amount out of range"); - // log(maxMPTokenAmount, 10) ~ 18.96 - if (mAsset.holds() && mOffset > 18) - Throw("MPT amount out of range"); - } + // log(cMaxNativeN, 10) == 17 + if (native() && mOffset > 17) + Throw("Native currency amount out of range"); + // log(maxMPTokenAmount, 10) ~ 18.96 + if (mAsset.holds() && mOffset > 18) + Throw("MPT amount out of range"); - if (getSTNumberSwitchover() && getSTAmountCanonicalizeSwitchover()) + if (getSTNumberSwitchover()) { Number num( mIsNegative ? -mValue : mValue, mOffset, Number::unchecked{}); @@ -919,16 +892,14 @@ STAmount::canonicalize() while (mOffset > 0) { - if (getSTAmountCanonicalizeSwitchover()) - { - // N.B. do not move the overflow check to after the - // multiplication - if (native() && mValue > cMaxNativeN) - Throw( - "Native currency amount out of range"); - else if (!native() && mValue > maxMPTokenAmount) - Throw("MPT amount out of range"); - } + // N.B. do not move the overflow check to after the + // multiplication + if (native() && mValue > cMaxNativeN) + Throw( + "Native currency amount out of range"); + else if (!native() && mValue > maxMPTokenAmount) + Throw("MPT amount out of range"); + mValue *= 10; --mOffset; } diff --git a/src/xrpld/app/misc/detail/TxQ.cpp b/src/xrpld/app/misc/detail/TxQ.cpp index 7c0a6f07e2..23a083fbe8 100644 --- a/src/xrpld/app/misc/detail/TxQ.cpp +++ b/src/xrpld/app/misc/detail/TxQ.cpp @@ -300,7 +300,6 @@ TxQ::MaybeTx::apply(Application& app, OpenView& view, beast::Journal j) // If the rules or flags change, preflight again XRPL_ASSERT( pfresult, "ripple::TxQ::MaybeTx::apply : preflight result is set"); - STAmountSO stAmountSO{view.rules().enabled(fixSTAmountCanonicalize)}; NumberSO stNumberSO{view.rules().enabled(fixUniversalNumber)}; if (pfresult->rules != view.rules() || pfresult->flags != flags) @@ -734,7 +733,6 @@ TxQ::apply( ApplyFlags flags, beast::Journal j) { - STAmountSO stAmountSO{view.rules().enabled(fixSTAmountCanonicalize)}; NumberSO stNumberSO{view.rules().enabled(fixUniversalNumber)}; // See if the transaction is valid, properly formed, diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index 2f62a142c0..6e3b56fe99 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -1163,9 +1163,8 @@ Transactor::operator()() { JLOG(j_.trace()) << "apply: " << ctx_.tx.getTransactionID(); - // raii classes for the current ledger rules. fixSTAmountCanonicalize and - // fixSTAmountCanonicalize predate the rulesGuard and should be replaced. - STAmountSO stAmountSO{view().rules().enabled(fixSTAmountCanonicalize)}; + // raii classes for the current ledger rules. + // fixUniversalNumber predate the rulesGuard and should be replaced. NumberSO stNumberSO{view().rules().enabled(fixUniversalNumber)}; CurrentTransactionRulesGuard currentTransctionRulesGuard(view().rules()); diff --git a/src/xrpld/app/tx/detail/apply.cpp b/src/xrpld/app/tx/detail/apply.cpp index e2e0adae45..0ddaf0578e 100644 --- a/src/xrpld/app/tx/detail/apply.cpp +++ b/src/xrpld/app/tx/detail/apply.cpp @@ -138,9 +138,7 @@ 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); } From efd4c1b95d329c93dd76e7e3d6ae99337362240c Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Thu, 30 Oct 2025 12:49:44 +0000 Subject: [PATCH 23/28] chore: Use new prepare-runner (#5970) See: XRPLF/actions#19. --- .github/workflows/reusable-build.yml | 2 +- .github/workflows/upload-conan-deps.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/reusable-build.yml b/.github/workflows/reusable-build.yml index d156f05394..a9d5fd7f7c 100644 --- a/.github/workflows/reusable-build.yml +++ b/.github/workflows/reusable-build.yml @@ -63,7 +63,7 @@ jobs: uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - name: Prepare runner - uses: XRPLF/actions/.github/actions/prepare-runner@638e0dc11ea230f91bd26622fb542116bb5254d5 + uses: XRPLF/actions/.github/actions/prepare-runner@99685816bb60a95a66852f212f382580e180df3a with: disable_ccache: false diff --git a/.github/workflows/upload-conan-deps.yml b/.github/workflows/upload-conan-deps.yml index aa3870ffcf..f6262a2f1f 100644 --- a/.github/workflows/upload-conan-deps.yml +++ b/.github/workflows/upload-conan-deps.yml @@ -66,7 +66,7 @@ jobs: uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - name: Prepare runner - uses: XRPLF/actions/.github/actions/prepare-runner@638e0dc11ea230f91bd26622fb542116bb5254d5 + uses: XRPLF/actions/.github/actions/prepare-runner@99685816bb60a95a66852f212f382580e180df3a with: disable_ccache: false From a10f42a3aa4e32943023adef7a13321823050cf3 Mon Sep 17 00:00:00 2001 From: Bart Date: Thu, 30 Oct 2025 09:19:51 -0400 Subject: [PATCH 24/28] ci: Check whether test failures are caused by port exhaustion (#5938) This change adds an extra step to the CI test job that outputs network info, which may allow us to confirm whether random test failures are caused by port exhaustion. --- .github/workflows/reusable-test.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/reusable-test.yml b/.github/workflows/reusable-test.yml index 2127355f40..8d4a4a8d33 100644 --- a/.github/workflows/reusable-test.yml +++ b/.github/workflows/reusable-test.yml @@ -100,3 +100,12 @@ jobs: "$test_file" fi done + + - name: Debug failure (Linux) + if: ${{ failure() && runner.os == 'Linux' && inputs.run_tests }} + shell: bash + run: | + echo "IPv4 local port range:" + cat /proc/sys/net/ipv4/ip_local_port_range + echo "Netstat:" + netstat -an From 44e027e51614c10124ab42588c65b63b173c1bf9 Mon Sep 17 00:00:00 2001 From: Pratik Mankawde Date: Thu, 30 Oct 2025 15:27:01 +0000 Subject: [PATCH 25/28] refactor: Retire fixTakerDryOfferRemoval amendment (#5958) Amendments activated for more than 2 years can be retired. This change retires the fixTakerDryOfferRemoval amendment. --- include/xrpl/protocol/detail/features.macro | 2 +- src/test/app/Offer_test.cpp | 25 +++++---------------- src/test/rpc/Feature_test.cpp | 4 ++-- 3 files changed, 8 insertions(+), 23 deletions(-) diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 0c817829db..6eb9c03267 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -101,7 +101,6 @@ XRPL_FIX (QualityUpperBound, Supported::yes, VoteBehavior::DefaultYe XRPL_FEATURE(DeletableAccounts, Supported::yes, VoteBehavior::DefaultYes) XRPL_FIX (PayChanRecipientOwnerDir, Supported::yes, VoteBehavior::DefaultYes) XRPL_FIX (MasterKeyAsRegularKey, Supported::yes, VoteBehavior::DefaultYes) -XRPL_FIX (TakerDryOfferRemoval, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(MultiSignReserve, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(DepositPreauth, Supported::yes, VoteBehavior::DefaultYes) XRPL_FIX (1623, Supported::yes, VoteBehavior::DefaultYes) @@ -144,6 +143,7 @@ XRPL_RETIRE(fix1781) XRPL_RETIRE(fixCheckThreading) XRPL_RETIRE(fixRmSmallIncreasedQOffers) XRPL_RETIRE(fixSTAmountCanonicalize) +XRPL_RETIRE(fixTakerDryOfferRemoval) XRPL_RETIRE(CryptoConditions) XRPL_RETIRE(Escrow) XRPL_RETIRE(EnforceInvariants) diff --git a/src/test/app/Offer_test.cpp b/src/test/app/Offer_test.cpp index c279afd2a2..0dbcfccebd 100644 --- a/src/test/app/Offer_test.cpp +++ b/src/test/app/Offer_test.cpp @@ -5294,14 +5294,12 @@ public: { using namespace jtx; static FeatureBitset const all{testable_amendments()}; - static FeatureBitset const takerDryOffer{fixTakerDryOfferRemoval}; static FeatureBitset const immediateOfferKilled{ featureImmediateOfferKilled}; FeatureBitset const fillOrKill{fixFillOrKill}; FeatureBitset const permDEX{featurePermissionedDEX}; - static std::array const feats{ - all - takerDryOffer - immediateOfferKilled - permDEX, + static std::array const feats{ all - immediateOfferKilled - permDEX, all - immediateOfferKilled - fillOrKill - permDEX, all - fillOrKill - permDEX, @@ -5323,7 +5321,7 @@ public: } }; -class OfferWTakerDryOffer_test : public OfferBaseUtil_test +class OfferWOSmallQOffers_test : public OfferBaseUtil_test { void run() override @@ -5332,7 +5330,7 @@ class OfferWTakerDryOffer_test : public OfferBaseUtil_test } }; -class OfferWOSmallQOffers_test : public OfferBaseUtil_test +class OfferWOFillOrKill_test : public OfferBaseUtil_test { void run() override @@ -5341,7 +5339,7 @@ class OfferWOSmallQOffers_test : public OfferBaseUtil_test } }; -class OfferWOFillOrKill_test : public OfferBaseUtil_test +class OfferWOPermDEX_test : public OfferBaseUtil_test { void run() override @@ -5350,21 +5348,12 @@ class OfferWOFillOrKill_test : public OfferBaseUtil_test } }; -class OfferWOPermDEX_test : public OfferBaseUtil_test -{ - void - run() override - { - OfferBaseUtil_test::run(4); - } -}; - class OfferAllFeatures_test : public OfferBaseUtil_test { void run() override { - OfferBaseUtil_test::run(5, true); + OfferBaseUtil_test::run(4, true); } }; @@ -5376,7 +5365,6 @@ class Offer_manual_test : public OfferBaseUtil_test using namespace jtx; FeatureBitset const all{testable_amendments()}; FeatureBitset const immediateOfferKilled{featureImmediateOfferKilled}; - FeatureBitset const takerDryOffer{fixTakerDryOfferRemoval}; FeatureBitset const fillOrKill{fixFillOrKill}; FeatureBitset const permDEX{featurePermissionedDEX}; @@ -5385,13 +5373,10 @@ class Offer_manual_test : public OfferBaseUtil_test testAll(all - fillOrKill - permDEX); testAll(all - permDEX); testAll(all); - - testAll(all - takerDryOffer - permDEX); } }; BEAST_DEFINE_TESTSUITE_PRIO(OfferBaseUtil, app, ripple, 2); -BEAST_DEFINE_TESTSUITE_PRIO(OfferWTakerDryOffer, app, ripple, 2); BEAST_DEFINE_TESTSUITE_PRIO(OfferWOSmallQOffers, app, ripple, 2); BEAST_DEFINE_TESTSUITE_PRIO(OfferWOFillOrKill, app, ripple, 2); BEAST_DEFINE_TESTSUITE_PRIO(OfferWOPermDEX, app, ripple, 2); diff --git a/src/test/rpc/Feature_test.cpp b/src/test/rpc/Feature_test.cpp index 895988152c..f01ee5b116 100644 --- a/src/test/rpc/Feature_test.cpp +++ b/src/test/rpc/Feature_test.cpp @@ -144,8 +144,8 @@ class Feature_test : public beast::unit_test::suite BEAST_EXPECT(featureToName(featureFlow) == "Flow"); BEAST_EXPECT(featureToName(featureNegativeUNL) == "NegativeUNL"); BEAST_EXPECT( - featureToName(fixTakerDryOfferRemoval) == - "fixTakerDryOfferRemoval"); + featureToName(fixIncludeKeyletFields) == "fixIncludeKeyletFields"); + BEAST_EXPECT(featureToName(featureTokenEscrow) == "TokenEscrow"); } void From b0910e359e4e8af70477ff93e4cde53b067a4b5a Mon Sep 17 00:00:00 2001 From: Pratik Mankawde Date: Thu, 30 Oct 2025 17:33:08 +0000 Subject: [PATCH 26/28] refactor: Retire fix1623 amendment (#5928) Amendments activated for more than 2 years can be retired. This change retires the fix1623 amendment. --- include/xrpl/protocol/detail/features.macro | 2 +- src/test/app/Check_test.cpp | 54 ++++++++------------- src/xrpld/app/tx/detail/CashCheck.cpp | 5 +- src/xrpld/rpc/detail/DeliveredAmount.cpp | 54 +++++---------------- 4 files changed, 35 insertions(+), 80 deletions(-) diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 6eb9c03267..61382cac13 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -103,7 +103,6 @@ XRPL_FIX (PayChanRecipientOwnerDir, Supported::yes, VoteBehavior::DefaultYe XRPL_FIX (MasterKeyAsRegularKey, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(MultiSignReserve, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(DepositPreauth, Supported::yes, VoteBehavior::DefaultYes) -XRPL_FIX (1623, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(Checks, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(DepositAuth, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(Flow, Supported::yes, VoteBehavior::DefaultYes) @@ -139,6 +138,7 @@ XRPL_RETIRE(fix1528) XRPL_RETIRE(fix1543) XRPL_RETIRE(fix1571) XRPL_RETIRE(fix1578) +XRPL_RETIRE(fix1623) XRPL_RETIRE(fix1781) XRPL_RETIRE(fixCheckThreading) XRPL_RETIRE(fixRmSmallIncreasedQOffers) diff --git a/src/test/app/Check_test.cpp b/src/test/app/Check_test.cpp index 74c8e9df6d..5d59e2ebd9 100644 --- a/src/test/app/Check_test.cpp +++ b/src/test/app/Check_test.cpp @@ -1867,49 +1867,35 @@ class Check_test : public beast::unit_test::suite } void - testFix1623Enable(FeatureBitset features) + testDeliveredAmountForCheckCashTxn(FeatureBitset features) { - testcase("Fix1623 enable"); + testcase("DeliveredAmount For CheckCash Txn"); using namespace test::jtx; + Account const alice{"alice"}; + Account const bob{"bob"}; - auto testEnable = [this]( - FeatureBitset const& features, bool hasFields) { - // Unless fix1623 is enabled a "tx" RPC command should return - // neither "DeliveredAmount" nor "delivered_amount" on a CheckCash - // transaction. - Account const alice{"alice"}; - Account const bob{"bob"}; + Env env{*this, features}; - Env env{*this, features}; + env.fund(XRP(1000), alice, bob); + env.close(); - env.fund(XRP(1000), alice, bob); - env.close(); + uint256 const chkId{getCheckIndex(alice, env.seq(alice))}; + env(check::create(alice, bob, XRP(200))); + env.close(); - uint256 const chkId{getCheckIndex(alice, env.seq(alice))}; - env(check::create(alice, bob, XRP(200))); - env.close(); + env(check::cash(bob, chkId, check::DeliverMin(XRP(100)))); - env(check::cash(bob, chkId, check::DeliverMin(XRP(100)))); + // Get the hash for the most recent transaction. + std::string const txHash{ + env.tx()->getJson(JsonOptions::none)[jss::hash].asString()}; - // Get the hash for the most recent transaction. - std::string const txHash{ - env.tx()->getJson(JsonOptions::none)[jss::hash].asString()}; + env.close(); + Json::Value const meta = env.rpc("tx", txHash)[jss::result][jss::meta]; - // DeliveredAmount and delivered_amount are either present or - // not present in the metadata returned by "tx" based on fix1623. - env.close(); - Json::Value const meta = - env.rpc("tx", txHash)[jss::result][jss::meta]; - - BEAST_EXPECT( - meta.isMember(sfDeliveredAmount.jsonName) == hasFields); - BEAST_EXPECT(meta.isMember(jss::delivered_amount) == hasFields); - }; - - // Run both the disabled and enabled cases. - testEnable(features - fix1623, false); - testEnable(features, true); + // DeliveredAmount and delivered_amount are present. + BEAST_EXPECT(meta.isMember(sfDeliveredAmount.jsonName)); + BEAST_EXPECT(meta.isMember(jss::delivered_amount)); } void @@ -2711,7 +2697,7 @@ class Check_test : public beast::unit_test::suite testCashInvalid(features); testCancelValid(features); testCancelInvalid(features); - testFix1623Enable(features); + testDeliveredAmountForCheckCashTxn(features); testWithTickets(features); } diff --git a/src/xrpld/app/tx/detail/CashCheck.cpp b/src/xrpld/app/tx/detail/CashCheck.cpp index 73dedba170..9107dece50 100644 --- a/src/xrpld/app/tx/detail/CashCheck.cpp +++ b/src/xrpld/app/tx/detail/CashCheck.cpp @@ -276,7 +276,6 @@ CashCheck::doApply() // work to do... auto viewJ = ctx_.app.journal("View"); auto const optDeliverMin = ctx_.tx[~sfDeliverMin]; - bool const doFix1623{psb.rules().enabled(fix1623)}; if (srcId != account_) { @@ -311,7 +310,7 @@ CashCheck::doApply() return tecUNFUNDED_PAYMENT; } - if (optDeliverMin && doFix1623) + if (optDeliverMin) // Set the DeliveredAmount metadata. ctx_.deliver(xrpDeliver); @@ -461,7 +460,7 @@ CashCheck::doApply() << "flow did not produce DeliverMin."; return tecPATH_PARTIAL; } - if (doFix1623 && !checkCashMakesTrustLine) + if (!checkCashMakesTrustLine) // Set the delivered_amount metadata. ctx_.deliver(result.actualAmountOut); } diff --git a/src/xrpld/rpc/detail/DeliveredAmount.cpp b/src/xrpld/rpc/detail/DeliveredAmount.cpp index 9a6d0e9dc8..b38210d19e 100644 --- a/src/xrpld/rpc/detail/DeliveredAmount.cpp +++ b/src/xrpld/rpc/detail/DeliveredAmount.cpp @@ -78,51 +78,25 @@ getDeliveredAmount( } // Returns true if transaction meta could contain a delivered amount field, -// based on transaction type, transaction result and whether fix1623 is enabled -template +// based on transaction type and transaction result bool -canHaveDeliveredAmountHelp( - GetFix1623Enabled const& getFix1623Enabled, +canHaveDeliveredAmount( std::shared_ptr const& serializedTx, TxMeta const& transactionMeta) { if (!serializedTx) return false; + TxType const tt{serializedTx->getTxnType()}; + // Transaction type should be ttPAYMENT, ttACCOUNT_DELETE or ttCHECK_CASH + // and if the transaction failed nothing could have been delivered. + if ((tt == ttPAYMENT || tt == ttCHECK_CASH || tt == ttACCOUNT_DELETE) && + transactionMeta.getResultTER() == tesSUCCESS) { - TxType const tt{serializedTx->getTxnType()}; - if (tt != ttPAYMENT && tt != ttCHECK_CASH && tt != ttACCOUNT_DELETE) - return false; - - if (tt == ttCHECK_CASH && !getFix1623Enabled()) - return false; + return true; } - // if the transaction failed nothing could have been delivered. - if (transactionMeta.getResultTER() != tesSUCCESS) - return false; - - return true; -} - -// Returns true if transaction meta could contain a delivered amount field, -// based on transaction type, transaction result and whether fix1623 is enabled -bool -canHaveDeliveredAmount( - RPC::Context const& context, - std::shared_ptr const& serializedTx, - TxMeta const& transactionMeta) -{ - // These lambdas are used to compute the values lazily - auto const getFix1623Enabled = [&context]() -> bool { - auto const view = context.app.openLedger().current(); - if (!view) - return false; - return view->rules().enabled(fix1623); - }; - - return canHaveDeliveredAmountHelp( - getFix1623Enabled, serializedTx, transactionMeta); + return false; } void @@ -133,12 +107,8 @@ insertDeliveredAmount( TxMeta const& transactionMeta) { auto const info = ledger.info(); - auto const getFix1623Enabled = [&ledger] { - return ledger.rules().enabled(fix1623); - }; - if (canHaveDeliveredAmountHelp( - getFix1623Enabled, serializedTx, transactionMeta)) + if (canHaveDeliveredAmount(serializedTx, transactionMeta)) { auto const getLedgerIndex = [&info] { return info.seq; }; auto const getCloseTime = [&info] { return info.closeTime; }; @@ -167,7 +137,7 @@ getDeliveredAmount( TxMeta const& transactionMeta, GetLedgerIndex const& getLedgerIndex) { - if (canHaveDeliveredAmount(context, serializedTx, transactionMeta)) + if (canHaveDeliveredAmount(serializedTx, transactionMeta)) { auto const getCloseTime = [&context, @@ -212,7 +182,7 @@ insertDeliveredAmount( std::shared_ptr const& transaction, TxMeta const& transactionMeta) { - if (canHaveDeliveredAmount(context, transaction, transactionMeta)) + if (canHaveDeliveredAmount(transaction, transactionMeta)) { auto amt = getDeliveredAmount( context, transaction, transactionMeta, [&transactionMeta]() { From b39d7a65199943f142ff6b87a7d726b82b5d637c Mon Sep 17 00:00:00 2001 From: Pratik Mankawde Date: Thu, 30 Oct 2025 18:47:47 +0000 Subject: [PATCH 27/28] refactor: Retire fixQualityUpperBound amendment (#5960) Amendments activated for more than 2 years can be retired. This change retires the fixQualityUpperBound amendment. --- include/xrpl/protocol/detail/features.macro | 8 +++++--- src/xrpld/app/paths/detail/DirectStep.cpp | 18 ------------------ 2 files changed, 5 insertions(+), 21 deletions(-) diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 61382cac13..c20f323e3d 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -97,7 +97,6 @@ XRPL_FEATURE(NegativeUNL, Supported::yes, VoteBehavior::DefaultYe XRPL_FIX (AmendmentMajorityCalc, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(HardenedValidations, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(RequireFullyCanonicalSig, Supported::yes, VoteBehavior::DefaultYes) -XRPL_FIX (QualityUpperBound, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(DeletableAccounts, Supported::yes, VoteBehavior::DefaultYes) XRPL_FIX (PayChanRecipientOwnerDir, Supported::yes, VoteBehavior::DefaultYes) XRPL_FIX (MasterKeyAsRegularKey, Supported::yes, VoteBehavior::DefaultYes) @@ -125,8 +124,10 @@ XRPL_FEATURE(CryptoConditionsSuite, Supported::yes, VoteBehavior::Obsolete) // 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 -// ledger must be registered either here or above with the "active" amendments +// All known amendments and amendments that may appear in a validated ledger +// must be registered either here or above with the "active" amendments +// +// Please keep this list sorted alphabetically for convenience. XRPL_RETIRE(fix1201) XRPL_RETIRE(fix1368) XRPL_RETIRE(fix1373) @@ -141,6 +142,7 @@ XRPL_RETIRE(fix1578) XRPL_RETIRE(fix1623) XRPL_RETIRE(fix1781) XRPL_RETIRE(fixCheckThreading) +XRPL_RETIRE(fixQualityUpperBound) XRPL_RETIRE(fixRmSmallIncreasedQOffers) XRPL_RETIRE(fixSTAmountCanonicalize) XRPL_RETIRE(fixTakerDryOfferRemoval) diff --git a/src/xrpld/app/paths/detail/DirectStep.cpp b/src/xrpld/app/paths/detail/DirectStep.cpp index a0808985b5..541b04b4c4 100644 --- a/src/xrpld/app/paths/detail/DirectStep.cpp +++ b/src/xrpld/app/paths/detail/DirectStep.cpp @@ -844,24 +844,6 @@ DirectStepI::qualityUpperBound( { auto const dir = this->debtDirection(v, StrandDirection::forward); - if (!v.rules().enabled(fixQualityUpperBound)) - { - std::uint32_t const srcQOut = [&]() -> std::uint32_t { - if (redeems(prevStepDir) && issues(dir)) - return transferRate(v, src_).value; - return QUALITY_ONE; - }(); - auto dstQIn = static_cast(this)->quality( - v, QualityDirection::in); - - if (isLast_ && dstQIn > QUALITY_ONE) - dstQIn = QUALITY_ONE; - Issue const iss{currency_, src_}; - return { - Quality(getRate(STAmount(iss, srcQOut), STAmount(iss, dstQIn))), - dir}; - } - auto const [srcQOut, dstQIn] = redeems(dir) ? qualitiesSrcRedeems(v) : qualitiesSrcIssues(v, prevStepDir); From 8d1b3b39940eca2735a0e9279bdd92b0748fd84c Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 30 Oct 2025 15:39:56 -0400 Subject: [PATCH 28/28] refactor: Add support for extra transaction signature validation (#5851) - Restructures `STTx` signature checking code to be able to handle a `sigObject`, which may be the full transaction, or may be an object field containing a separate signature. Either way, the `sigObject` can be a single- or multi-sign signature. - This is distinct from 550f90a75e (#5594), which changed the check in Transactor, which validates whether a given account is allowed to sign for the given transaction. This cryptographically checks the signature validity. --- include/xrpl/protocol/STObject.h | 5 + include/xrpl/protocol/STTx.h | 48 ++++++-- include/xrpl/protocol/jss.h | 1 + src/libxrpl/protocol/STObject.cpp | 16 +++ src/libxrpl/protocol/STTx.cpp | 140 +++++++++++++++-------- src/test/jtx/JTx.h | 6 +- src/test/jtx/TestHelpers.h | 2 +- src/test/jtx/amount.h | 8 +- src/test/jtx/impl/Env.cpp | 19 ++- src/test/jtx/impl/mpt.cpp | 2 +- src/test/jtx/impl/multisign.cpp | 17 ++- src/test/jtx/impl/sig.cpp | 14 ++- src/test/jtx/impl/utility.cpp | 12 +- src/test/jtx/mpt.h | 2 +- src/test/jtx/multisign.h | 53 ++++++++- src/test/jtx/sig.h | 25 +++- src/test/jtx/utility.h | 6 + src/test/rpc/RPCCall_test.cpp | 98 ++++++++++++---- src/xrpld/app/main/Main.cpp | 2 +- src/xrpld/app/tx/detail/Batch.cpp | 74 ++++++++---- src/xrpld/rpc/detail/RPCCall.cpp | 20 +++- src/xrpld/rpc/detail/TransactionSign.cpp | 71 ++++++++++-- 22 files changed, 511 insertions(+), 130 deletions(-) diff --git a/include/xrpl/protocol/STObject.h b/include/xrpl/protocol/STObject.h index 1c22b08aba..84706c5833 100644 --- a/include/xrpl/protocol/STObject.h +++ b/include/xrpl/protocol/STObject.h @@ -244,6 +244,9 @@ public: getFieldPathSet(SField const& field) const; STVector256 const& getFieldV256(SField const& field) const; + // If not found, returns an object constructed with the given field + STObject + getFieldObject(SField const& field) const; STArray const& getFieldArray(SField const& field) const; STCurrency const& @@ -390,6 +393,8 @@ public: setFieldV256(SField const& field, STVector256 const& v); void setFieldArray(SField const& field, STArray const& v); + void + setFieldObject(SField const& field, STObject const& v); template void diff --git a/include/xrpl/protocol/STTx.h b/include/xrpl/protocol/STTx.h index f0d2157283..ac0223ee77 100644 --- a/include/xrpl/protocol/STTx.h +++ b/include/xrpl/protocol/STTx.h @@ -87,8 +87,14 @@ public: getFullText() const override; // Outer transaction functions / signature functions. + static Blob + getSignature(STObject const& sigObject); + Blob - getSignature() const; + getSignature() const + { + return getSignature(*this); + } uint256 getSigningHash() const; @@ -119,13 +125,20 @@ public: getJson(JsonOptions options, bool binary) const; void - sign(PublicKey const& publicKey, SecretKey const& secretKey); + sign( + PublicKey const& publicKey, + SecretKey const& secretKey, + std::optional> signatureTarget = + {}); - /** Check the signature. - @return `true` if valid signature. If invalid, the error message string. - */ enum class RequireFullyCanonicalSig : bool { no, yes }; + /** Check the signature. + @param requireCanonicalSig If `true`, check that the signature is fully + canonical. If `false`, only check that the signature is valid. + @param rules The current ledger rules. + @return `true` if valid signature. If invalid, the error message string. + */ Expected checkSign(RequireFullyCanonicalSig requireCanonicalSig, Rules const& rules) const; @@ -150,17 +163,34 @@ public: char status, std::string const& escapedMetaData) const; - std::vector + std::vector const& getBatchTransactionIDs() const; private: + /** Check the signature. + @param requireCanonicalSig If `true`, check that the signature is fully + canonical. If `false`, only check that the signature is valid. + @param rules The current ledger rules. + @param sigObject Reference to object that contains the signature fields. + Will be *this more often than not. + @return `true` if valid signature. If invalid, the error message string. + */ Expected - checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const; + checkSign( + RequireFullyCanonicalSig requireCanonicalSig, + Rules const& rules, + STObject const& sigObject) const; + + Expected + checkSingleSign( + RequireFullyCanonicalSig requireCanonicalSig, + STObject const& sigObject) const; Expected checkMultiSign( RequireFullyCanonicalSig requireCanonicalSig, - Rules const& rules) const; + Rules const& rules, + STObject const& sigObject) const; Expected checkBatchSingleSign( @@ -179,7 +209,7 @@ private: move(std::size_t n, void* buf) override; friend class detail::STVar; - mutable std::vector batch_txn_ids_; + mutable std::vector batchTxnIds_; }; bool diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index 8609aedaef..bb5af1fc4d 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -569,6 +569,7 @@ JSS(settle_delay); // out: AccountChannels JSS(severity); // in: LogLevel JSS(shares); // out: VaultInfo JSS(signature); // out: NetworkOPs, ChannelAuthorize +JSS(signature_target); // in: TransactionSign JSS(signature_verified); // out: ChannelVerify JSS(signing_key); // out: NetworkOPs JSS(signing_keys); // out: ValidatorList diff --git a/src/libxrpl/protocol/STObject.cpp b/src/libxrpl/protocol/STObject.cpp index 77e5fd1ad9..8fb21a638d 100644 --- a/src/libxrpl/protocol/STObject.cpp +++ b/src/libxrpl/protocol/STObject.cpp @@ -688,6 +688,16 @@ STObject::getFieldV256(SField const& field) const return getFieldByConstRef(field, empty); } +STObject +STObject::getFieldObject(SField const& field) const +{ + STObject const empty{field}; + auto ret = getFieldByConstRef(field, empty); + if (ret != empty) + ret.applyTemplateFromSField(field); + return ret; +} + STArray const& STObject::getFieldArray(SField const& field) const { @@ -833,6 +843,12 @@ STObject::setFieldArray(SField const& field, STArray const& v) setFieldUsingAssignment(field, v); } +void +STObject::setFieldObject(SField const& field, STObject const& v) +{ + setFieldUsingAssignment(field, v); +} + Json::Value STObject::getJson(JsonOptions options) const { diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index 8be8f906a5..7326f48424 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -200,11 +200,11 @@ STTx::getSigningHash() const } Blob -STTx::getSignature() const +STTx::getSignature(STObject const& sigObject) { try { - return getFieldVL(sfTxnSignature); + return sigObject.getFieldVL(sfTxnSignature); } catch (std::exception const&) { @@ -234,35 +234,68 @@ STTx::getSeqValue() const } void -STTx::sign(PublicKey const& publicKey, SecretKey const& secretKey) +STTx::sign( + PublicKey const& publicKey, + SecretKey const& secretKey, + std::optional> signatureTarget) { auto const data = getSigningData(*this); auto const sig = ripple::sign(publicKey, secretKey, makeSlice(data)); - setFieldVL(sfTxnSignature, sig); + if (signatureTarget) + { + auto& target = peekFieldObject(*signatureTarget); + target.setFieldVL(sfTxnSignature, sig); + } + else + { + setFieldVL(sfTxnSignature, sig); + } tid_ = getHash(HashPrefix::transactionID); } +Expected +STTx::checkSign( + RequireFullyCanonicalSig requireCanonicalSig, + Rules const& rules, + STObject const& sigObject) const +{ + try + { + // Determine whether we're single- or multi-signing by looking + // at the SigningPubKey. If it's empty we must be + // multi-signing. Otherwise we're single-signing. + + Blob const& signingPubKey = sigObject.getFieldVL(sfSigningPubKey); + return signingPubKey.empty() + ? checkMultiSign(requireCanonicalSig, rules, sigObject) + : checkSingleSign(requireCanonicalSig, sigObject); + } + catch (std::exception const&) + { + } + return Unexpected("Internal signature check failure."); +} + Expected STTx::checkSign( RequireFullyCanonicalSig requireCanonicalSig, Rules const& rules) const { - try + if (auto const ret = checkSign(requireCanonicalSig, rules, *this); !ret) + return ret; + + /* Placeholder for field that will be added by Lending Protocol + if (isFieldPresent(sfCounterpartySignature)) { - // Determine whether we're single- or multi-signing by looking - // at the SigningPubKey. If it's empty we must be - // multi-signing. Otherwise we're single-signing. - Blob const& signingPubKey = getFieldVL(sfSigningPubKey); - return signingPubKey.empty() - ? checkMultiSign(requireCanonicalSig, rules) - : checkSingleSign(requireCanonicalSig); + auto const counterSig = getFieldObject(sfCounterpartySignature); + if (auto const ret = checkSign(requireCanonicalSig, rules, counterSig); + !ret) + return Unexpected("Counterparty: " + ret.error()); } - catch (std::exception const&) - { - } - return Unexpected("Internal signature check failure."); + */ + return {}; } Expected @@ -382,23 +415,23 @@ STTx::getMetaSQL( static Expected singleSignHelper( - STObject const& signer, + STObject const& sigObject, 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 (signer.isFieldPresent(sfSigners)) + if (sigObject.isFieldPresent(sfSigners)) return Unexpected("Cannot both single- and multi-sign."); bool validSig = false; try { - auto const spk = signer.getFieldVL(sfSigningPubKey); + auto const spk = sigObject.getFieldVL(sfSigningPubKey); if (publicKeyType(makeSlice(spk))) { - Blob const signature = signer.getFieldVL(sfTxnSignature); + Blob const signature = sigObject.getFieldVL(sfTxnSignature); validSig = verify( PublicKey(makeSlice(spk)), data, @@ -418,12 +451,14 @@ singleSignHelper( } Expected -STTx::checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const +STTx::checkSingleSign( + RequireFullyCanonicalSig requireCanonicalSig, + STObject const& sigObject) const { auto const data = getSigningData(*this); bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) || (requireCanonicalSig == STTx::RequireFullyCanonicalSig::yes); - return singleSignHelper(*this, makeSlice(data), fullyCanonical); + return singleSignHelper(sigObject, makeSlice(data), fullyCanonical); } Expected @@ -440,31 +475,29 @@ STTx::checkBatchSingleSign( Expected multiSignHelper( - STObject const& signerObj, + STObject const& sigObject, + std::optional txnAccountID, 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 (!signerObj.isFieldPresent(sfSigners)) + if (!sigObject.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 (signerObj.isFieldPresent(sfTxnSignature)) + if (sigObject.isFieldPresent(sfTxnSignature)) return Unexpected("Cannot both single- and multi-sign."); - STArray const& signers{signerObj.getFieldArray(sfSigners)}; + STArray const& signers{sigObject.getFieldArray(sfSigners)}; // There are well known bounds that the number of signers must be within. if (signers.size() < STTx::minMultiSigners || signers.size() > STTx::maxMultiSigners(&rules)) return Unexpected("Invalid Signers array size."); - // We also use the sfAccount field inside the loop. Get it once. - auto const txnAccountID = signerObj.getAccountID(sfAccount); - // Signers must be in sorted order by AccountID. AccountID lastAccountID(beast::zero); @@ -472,8 +505,10 @@ multiSignHelper( { auto const accountID = signer.getAccountID(sfAccount); - // The account owner may not multisign for themselves. - if (accountID == txnAccountID) + // The account owner may not usually multisign for themselves. + // If they can, txnAccountID will be unseated, which is not equal to any + // value. + if (txnAccountID == accountID) return Unexpected("Invalid multisigner."); // No duplicate signers allowed. @@ -489,6 +524,7 @@ multiSignHelper( // Verify the signature. bool validSig = false; + std::optional errorWhat; try { auto spk = signer.getFieldVL(sfSigningPubKey); @@ -502,15 +538,16 @@ multiSignHelper( fullyCanonical); } } - catch (std::exception const&) + catch (std::exception const& e) { // We assume any problem lies with the signature. validSig = false; + errorWhat = e.what(); } if (!validSig) return Unexpected( std::string("Invalid signature on account ") + - toBase58(accountID) + "."); + toBase58(accountID) + errorWhat.value_or("") + "."); } // All signatures verified. return {}; @@ -532,8 +569,9 @@ STTx::checkBatchMultiSign( serializeBatch(dataStart, getFlags(), getBatchTransactionIDs()); return multiSignHelper( batchSigner, + std::nullopt, fullyCanonical, - [&dataStart](AccountID const& accountID) mutable -> Serializer { + [&dataStart](AccountID const& accountID) -> Serializer { Serializer s = dataStart; finishMultiSigningData(accountID, s); return s; @@ -544,19 +582,27 @@ STTx::checkBatchMultiSign( Expected STTx::checkMultiSign( RequireFullyCanonicalSig requireCanonicalSig, - Rules const& rules) const + Rules const& rules, + STObject const& sigObject) const { bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) || (requireCanonicalSig == RequireFullyCanonicalSig::yes); + // Used inside the loop in multiSignHelper to enforce that + // the account owner may not multisign for themselves. + auto const txnAccountID = &sigObject != this + ? std::nullopt + : std::optional(getAccountID(sfAccount)); + // 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, + sigObject, + txnAccountID, fullyCanonical, - [&dataStart](AccountID const& accountID) mutable -> Serializer { + [&dataStart](AccountID const& accountID) -> Serializer { Serializer s = dataStart; finishMultiSigningData(accountID, s); return s; @@ -569,7 +615,7 @@ STTx::checkMultiSign( * * 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_`, + * transaction IDs have already been computed and cached in `batchTxnIds_`, * it returns the cached vector. Otherwise, it computes the transaction IDs, * caches them, and then returns the vector. * @@ -579,7 +625,7 @@ STTx::checkMultiSign( * empty and that the size of the computed batch transaction IDs matches the * size of the `sfRawTransactions` field array. */ -std::vector +std::vector const& STTx::getBatchTransactionIDs() const { XRPL_ASSERT( @@ -588,16 +634,20 @@ STTx::getBatchTransactionIDs() const 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)); + // The list of inner ids is built once, then reused on subsequent calls. + // After the list is built, it must always have the same size as the array + // `sfRawTransactions`. The assert below verifies that. + if (batchTxnIds_.size() == 0) + { + for (STObject const& rb : getFieldArray(sfRawTransactions)) + batchTxnIds_.push_back(rb.getHash(HashPrefix::transactionID)); + } XRPL_ASSERT( - batch_txn_ids_.size() == getFieldArray(sfRawTransactions).size(), + batchTxnIds_.size() == getFieldArray(sfRawTransactions).size(), "STTx::getBatchTransactionIDs : batch transaction IDs size mismatch"); - return batch_txn_ids_; + return batchTxnIds_; } //------------------------------------------------------------------------------ diff --git a/src/test/jtx/JTx.h b/src/test/jtx/JTx.h index 198839dd28..36127f1843 100644 --- a/src/test/jtx/JTx.h +++ b/src/test/jtx/JTx.h @@ -54,7 +54,11 @@ struct JTx bool fill_sig = true; bool fill_netid = true; std::shared_ptr stx; - std::function signer; + // Functions that sign the transaction from the Account + std::vector> mainSigners; + // Functions that sign something else after the mainSigners, such as + // sfCounterpartySignature + std::vector> postSigners; JTx() = default; JTx(JTx const&) = default; diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index a7bdbb9b9f..5d919add47 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -618,7 +618,7 @@ create( } // namespace check -static constexpr FeeLevel64 baseFeeLevel{256}; +static constexpr FeeLevel64 baseFeeLevel{TxQ::baseLevel}; static constexpr FeeLevel64 minEscalationFeeLevel = baseFeeLevel * 500; template diff --git a/src/test/jtx/amount.h b/src/test/jtx/amount.h index a793f3a287..f14a65f6b8 100644 --- a/src/test/jtx/amount.h +++ b/src/test/jtx/amount.h @@ -213,14 +213,16 @@ public: template PrettyAmount - operator()(T v) const + operator()(T v, Number::rounding_mode rounding = Number::getround()) const { - return operator()(Number(v)); + return operator()(Number(v), rounding); } PrettyAmount - operator()(Number v) const + operator()(Number v, Number::rounding_mode rounding = Number::getround()) + const { + NumberRoundModeGuard mg(rounding); STAmount amount{asset_, v * scale_}; return {amount, ""}; } diff --git a/src/test/jtx/impl/Env.cpp b/src/test/jtx/impl/Env.cpp index f17f6cb39d..ce5f6f150c 100644 --- a/src/test/jtx/impl/Env.cpp +++ b/src/test/jtx/impl/Env.cpp @@ -34,6 +34,7 @@ #include #include +#include #include #include #include @@ -531,8 +532,22 @@ void Env::autofill_sig(JTx& jt) { auto& jv = jt.jv; - if (jt.signer) - return jt.signer(*this, jt); + + scope_success success([&]() { + // Call all the post-signers after the main signers or autofill are done + for (auto const& signer : jt.postSigners) + signer(*this, jt); + }); + + // Call all the main signers + if (!jt.mainSigners.empty()) + { + for (auto const& signer : jt.mainSigners) + signer(*this, jt); + return; + } + + // If the sig is still needed, get it here. if (!jt.fill_sig) return; auto const account = jv.isMember(sfDelegate.jsonName) diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index f2f51492e3..aaa5e433f2 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -507,7 +507,7 @@ MPTTester::getFlags(std::optional const& holder) const } MPT -MPTTester::operator[](std::string const& name) +MPTTester::operator[](std::string const& name) const { return MPT(name, issuanceID()); } diff --git a/src/test/jtx/impl/multisign.cpp b/src/test/jtx/impl/multisign.cpp index 6ed6df6804..fc6163a2f3 100644 --- a/src/test/jtx/impl/multisign.cpp +++ b/src/test/jtx/impl/multisign.cpp @@ -69,8 +69,15 @@ void msig::operator()(Env& env, JTx& jt) const { auto const mySigners = signers; - jt.signer = [mySigners, &env](Env&, JTx& jtx) { - jtx[sfSigningPubKey.getJsonName()] = ""; + auto callback = [subField = subField, mySigners, &env](Env&, JTx& jtx) { + // Where to put the signature. Supports sfCounterPartySignature. + auto& sigObject = subField ? jtx[*subField] : jtx.jv; + + // The signing pub key is only required at the top level. + if (!subField) + sigObject[sfSigningPubKey] = ""; + else if (sigObject.isNull()) + sigObject = Json::Value(Json::objectValue); std::optional st; try { @@ -81,7 +88,7 @@ msig::operator()(Env& env, JTx& jt) const env.test.log << pretty(jtx.jv) << std::endl; Rethrow(); } - auto& js = jtx[sfSigners.getJsonName()]; + auto& js = sigObject[sfSigners]; for (std::size_t i = 0; i < mySigners.size(); ++i) { auto const& e = mySigners[i]; @@ -96,6 +103,10 @@ msig::operator()(Env& env, JTx& jt) const strHex(Slice{sig.data(), sig.size()}); } }; + if (!subField) + jt.mainSigners.emplace_back(callback); + else + jt.postSigners.emplace_back(callback); } } // namespace jtx diff --git a/src/test/jtx/impl/sig.cpp b/src/test/jtx/impl/sig.cpp index fa1977fe08..6ea1c153cb 100644 --- a/src/test/jtx/impl/sig.cpp +++ b/src/test/jtx/impl/sig.cpp @@ -29,12 +29,22 @@ sig::operator()(Env&, JTx& jt) const { if (!manual_) return; - jt.fill_sig = false; + if (!subField_) + jt.fill_sig = false; if (account_) { // VFALCO Inefficient pre-C++14 auto const account = *account_; - jt.signer = [account](Env&, JTx& jtx) { jtx::sign(jtx.jv, account); }; + auto callback = [subField = subField_, account](Env&, JTx& jtx) { + // Where to put the signature. Supports sfCounterPartySignature. + auto& sigObject = subField ? jtx[*subField] : jtx.jv; + + jtx::sign(jtx.jv, account, sigObject); + }; + if (!subField_) + jt.mainSigners.emplace_back(callback); + else + jt.postSigners.emplace_back(callback); } } diff --git a/src/test/jtx/impl/utility.cpp b/src/test/jtx/impl/utility.cpp index 27b45a32cb..fbdbaee4ef 100644 --- a/src/test/jtx/impl/utility.cpp +++ b/src/test/jtx/impl/utility.cpp @@ -44,14 +44,20 @@ parse(Json::Value const& jv) } void -sign(Json::Value& jv, Account const& account) +sign(Json::Value& jv, Account const& account, Json::Value& sigObject) { - jv[jss::SigningPubKey] = strHex(account.pk().slice()); + sigObject[jss::SigningPubKey] = strHex(account.pk().slice()); Serializer ss; ss.add32(HashPrefix::txSign); parse(jv).addWithoutSigningFields(ss); auto const sig = ripple::sign(account.pk(), account.sk(), ss.slice()); - jv[jss::TxnSignature] = strHex(Slice{sig.data(), sig.size()}); + sigObject[jss::TxnSignature] = strHex(Slice{sig.data(), sig.size()}); +} + +void +sign(Json::Value& jv, Account const& account) +{ + sign(jv, account, jv); } void diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h index 2eacac68ec..422afa8fab 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -235,7 +235,7 @@ public: getBalance(Account const& account) const; MPT - operator[](std::string const& name); + operator[](std::string const& name) const; private: using SLEP = std::shared_ptr; diff --git a/src/test/jtx/multisign.h b/src/test/jtx/multisign.h index 1fed895c6d..7a035a9ff0 100644 --- a/src/test/jtx/multisign.h +++ b/src/test/jtx/multisign.h @@ -67,18 +67,63 @@ class msig { public: std::vector signers; + /** Alternative transaction object field in which to place the signer list. + * + * subField is only supported if an account_ is provided as well. + */ + SField const* const subField = nullptr; + /// Used solely as a convenience placeholder for ctors that do _not_ specify + /// a subfield. + static constexpr SField* const topLevel = nullptr; - msig(std::vector signers_) : signers(std::move(signers_)) + msig(SField const* subField_, std::vector signers_) + : signers(std::move(signers_)), subField(subField_) { sortSigners(signers); } + msig(SField const& subField_, std::vector signers_) + : msig{&subField_, signers_} + { + } + + msig(std::vector signers_) : msig(topLevel, signers_) + { + } + template requires std::convertible_to - explicit msig(AccountType&& a0, Accounts&&... aN) - : signers{std::forward(a0), std::forward(aN)...} + explicit msig(SField const* subField_, AccountType&& a0, Accounts&&... aN) + : msig{ + subField_, + std::vector{ + std::forward(a0), + std::forward(aN)...}} + { + } + + template + requires std::convertible_to + explicit msig(SField const& subField_, AccountType&& a0, Accounts&&... aN) + : msig{ + &subField_, + std::vector{ + std::forward(a0), + std::forward(aN)...}} + { + } + + template + requires( + std::convertible_to && + !std::is_same_v) + explicit msig(AccountType&& a0, Accounts&&... aN) + : msig{ + topLevel, + std::vector{ + std::forward(a0), + std::forward(aN)...}} { - sortSigners(signers); } void diff --git a/src/test/jtx/sig.h b/src/test/jtx/sig.h index aa65a4f697..b96a306a37 100644 --- a/src/test/jtx/sig.h +++ b/src/test/jtx/sig.h @@ -35,7 +35,20 @@ class sig { private: bool manual_ = true; + /** Alternative transaction object field in which to place the signature. + * + * subField is only supported if an account_ is provided as well. + */ + SField const* const subField_ = nullptr; + /** Account that will generate the signature. + * + * If not provided, no signature will be added by this helper. See also + * Env::autofill_sig. + */ std::optional account_; + /// Used solely as a convenience placeholder for ctors that do _not_ specify + /// a subfield. + static constexpr SField* const topLevel = nullptr; public: explicit sig(autofill_t) : manual_(false) @@ -46,7 +59,17 @@ public: { } - explicit sig(Account const& account) : account_(account) + explicit sig(SField const* subField, Account const& account) + : subField_(subField), account_(account) + { + } + + explicit sig(SField const& subField, Account const& account) + : sig(&subField, account) + { + } + + explicit sig(Account const& account) : sig(topLevel, account) { } diff --git a/src/test/jtx/utility.h b/src/test/jtx/utility.h index 2c21fbd3ff..9e89c3bb93 100644 --- a/src/test/jtx/utility.h +++ b/src/test/jtx/utility.h @@ -51,6 +51,12 @@ struct parse_error : std::logic_error STObject parse(Json::Value const& jv); +/** Sign automatically into a specific Json field of the jv object. + @note This only works on accounts with multi-signing off. +*/ +void +sign(Json::Value& jv, Account const& account, Json::Value& sigObject); + /** Sign automatically. @note This only works on accounts with multi-signing off. */ diff --git a/src/test/rpc/RPCCall_test.cpp b/src/test/rpc/RPCCall_test.cpp index d22896388d..26a6a536ef 100644 --- a/src/test/rpc/RPCCall_test.cpp +++ b/src/test/rpc/RPCCall_test.cpp @@ -4643,10 +4643,34 @@ static RPCCallTestData const rpcCallTestArray[] = { } ] })"}, - {"sign: too many arguments.", + {"sign: offline flag with signature_target.", __LINE__, {"sign", "my_secret", R"({"json_argument":true})", "offline", "extra"}, RPCCallTestData::no_exception, + R"({ + "method" : "sign", + "params" : [ + { + "api_version" : %API_VER%, + "offline" : true, + "secret" : "my_secret", + "signature_target" : "extra", + "tx_json" : + { + "json_argument" : true + } + } + ] + })"}, + {"sign: too many arguments.", + __LINE__, + {"sign", + "my_secret", + R"({"json_argument":true})", + "offline", + "CounterpartySignature", + "extra"}, + RPCCallTestData::no_exception, R"({ "method" : "sign", "params" : [ @@ -4675,20 +4699,24 @@ static RPCCallTestData const rpcCallTestArray[] = { } ] })"}, - {"sign: invalid final argument.", + {"sign: misspelled offline flag interpreted as signature_target.", __LINE__, {"sign", "my_secret", R"({"json_argument":true})", "offlin"}, RPCCallTestData::no_exception, R"({ - "method" : "sign", - "params" : [ - { - "error" : "invalidParams", - "error_code" : 31, - "error_message" : "Invalid parameters." - } - ] - })"}, + "method" : "sign", + "params" : [ + { + "api_version" : %API_VER%, + "secret" : "my_secret", + "signature_target" : "offlin", + "tx_json" : + { + "json_argument" : true + } + } + ] + })"}, // sign_for // -------------------------------------------------------------------- @@ -4880,10 +4908,34 @@ static RPCCallTestData const rpcCallTestArray[] = { } ] })"}, - {"submit: too many arguments.", + {"submit: offline flag with signature_target.", __LINE__, {"submit", "my_secret", R"({"json_argument":true})", "offline", "extra"}, RPCCallTestData::no_exception, + R"({ + "method" : "submit", + "params" : [ + { + "api_version" : %API_VER%, + "offline" : true, + "secret" : "my_secret", + "signature_target" : "extra", + "tx_json" : + { + "json_argument" : true + } + } + ] + })"}, + {"submit: too many arguments.", + __LINE__, + {"submit", + "my_secret", + R"({"json_argument":true})", + "offline", + "CounterpartySignature", + "extra"}, + RPCCallTestData::no_exception, R"({ "method" : "submit", "params" : [ @@ -4912,19 +4964,23 @@ static RPCCallTestData const rpcCallTestArray[] = { } ] })"}, - {"submit: last argument not \"offline\".", + {"submit: misspelled offline flag interpreted as signature_target.", __LINE__, {"submit", "my_secret", R"({"json_argument":true})", "offlne"}, RPCCallTestData::no_exception, R"({ - "method" : "submit", - "params" : [ - { - "error" : "invalidParams", - "error_code" : 31, - "error_message" : "Invalid parameters." - } - ] + "method" : "submit", + "params" : [ + { + "api_version" : %API_VER%, + "secret" : "my_secret", + "signature_target" : "offlne", + "tx_json" : + { + "json_argument" : true + } + } + ] })"}, // submit_multisigned diff --git a/src/xrpld/app/main/Main.cpp b/src/xrpld/app/main/Main.cpp index 2353d7acd1..3dff9d4d5f 100644 --- a/src/xrpld/app/main/Main.cpp +++ b/src/xrpld/app/main/Main.cpp @@ -173,7 +173,7 @@ printHelp(po::options_description const& desc) " server_state [counters]\n" " sign [offline]\n" " sign_for " - "[offline]\n" + "[offline] []\n" " stop\n" " simulate [|] []\n" " submit |[ ]\n" diff --git a/src/xrpld/app/tx/detail/Batch.cpp b/src/xrpld/app/tx/detail/Batch.cpp index cba89348d0..4ffdd2d57c 100644 --- a/src/xrpld/app/tx/detail/Batch.cpp +++ b/src/xrpld/app/tx/detail/Batch.cpp @@ -237,6 +237,39 @@ Batch::preflight(PreflightContext const& ctx) std::unordered_set uniqueHashes; std::unordered_map> accountSeqTicket; + auto checkSignatureFields = [&parentBatchId, &j = ctx.j]( + STObject const& sig, + uint256 const& hash, + char const* label = "") -> NotTEC { + if (sig.isFieldPresent(sfTxnSignature)) + { + JLOG(j.debug()) + << "BatchTrace[" << parentBatchId << "]: " + << "inner txn " << label << "cannot include TxnSignature. " + << "txID: " << hash; + return temBAD_SIGNATURE; + } + + if (sig.isFieldPresent(sfSigners)) + { + JLOG(j.debug()) + << "BatchTrace[" << parentBatchId << "]: " + << "inner txn " << label << " cannot include Signers. " + << "txID: " << hash; + return temBAD_SIGNER; + } + + if (!sig.getFieldVL(sfSigningPubKey).empty()) + { + JLOG(j.debug()) + << "BatchTrace[" << parentBatchId << "]: " + << "inner txn " << label << " SigningPubKey must be empty. " + << "txID: " << hash; + return temBAD_REGKEY; + } + + return tesSUCCESS; + }; for (STObject rb : rawTxns) { STTx const stx = STTx{std::move(rb)}; @@ -266,29 +299,23 @@ Batch::preflight(PreflightContext const& ctx) return temINVALID_FLAG; } - if (stx.isFieldPresent(sfTxnSignature)) - { - JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " - << "inner txn cannot include TxnSignature. " - << "txID: " << hash; - return temBAD_SIGNATURE; - } + if (auto const ret = checkSignatureFields(stx, hash)) + return ret; - if (stx.isFieldPresent(sfSigners)) + /* Placeholder for field that will be added by Lending Protocol + // Note that the CounterpartySignature is optional, and should not be + // included, but if it is, ensure it doesn't contain a signature. + if (stx.isFieldPresent(sfCounterpartySignature)) { - 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 counterpartySignature = + stx.getFieldObject(sfCounterpartySignature); + if (auto const ret = checkSignatureFields( + counterpartySignature, hash, "counterparty signature ")) + { + return ret; + } } + */ auto const innerAccount = stx.getAccountID(sfAccount); if (auto const preflightResult = ripple::preflight( @@ -385,6 +412,13 @@ Batch::preflightSigValidated(PreflightContext const& ctx) // inner account to the required signers set. if (innerAccount != outerAccount) requiredSigners.insert(innerAccount); + /* Placeholder for field that will be added by Lending Protocol + // Some transactions have a Counterparty, who must also sign the + // transaction if they are not the outer account + if (auto const counterparty = rb.at(~sfCounterparty); + counterparty && counterparty != outerAccount) + requiredSigners.insert(*counterparty); + */ } // Validation Batch Signers diff --git a/src/xrpld/rpc/detail/RPCCall.cpp b/src/xrpld/rpc/detail/RPCCall.cpp index 57432d920f..822eb440b3 100644 --- a/src/xrpld/rpc/detail/RPCCall.cpp +++ b/src/xrpld/rpc/detail/RPCCall.cpp @@ -965,7 +965,16 @@ private: Json::Value txJSON; Json::Reader reader; bool const bOffline = - 3 == jvParams.size() && jvParams[2u].asString() == "offline"; + jvParams.size() >= 3 && jvParams[2u].asString() == "offline"; + std::optional const field = + [&jvParams, bOffline]() -> std::optional { + if (jvParams.size() < 3) + return std::nullopt; + if (jvParams.size() < 4 && bOffline) + return std::nullopt; + Json::UInt index = bOffline ? 3u : 2u; + return jvParams[index].asString(); + }(); if (1 == jvParams.size()) { @@ -978,7 +987,7 @@ private: return jvRequest; } else if ( - (2 == jvParams.size() || bOffline) && + (jvParams.size() >= 2 || bOffline) && reader.parse(jvParams[1u].asString(), txJSON)) { // Signing or submitting tx_json. @@ -990,6 +999,9 @@ private: if (bOffline) jvRequest[jss::offline] = true; + if (field) + jvRequest[jss::signature_target] = *field; + return jvRequest; } @@ -1270,11 +1282,11 @@ public: {"server_definitions", &RPCParser::parseServerDefinitions, 0, 1}, {"server_info", &RPCParser::parseServerInfo, 0, 1}, {"server_state", &RPCParser::parseServerInfo, 0, 1}, - {"sign", &RPCParser::parseSignSubmit, 2, 3}, + {"sign", &RPCParser::parseSignSubmit, 2, 4}, {"sign_for", &RPCParser::parseSignFor, 3, 4}, {"stop", &RPCParser::parseAsIs, 0, 0}, {"simulate", &RPCParser::parseSimulate, 1, 2}, - {"submit", &RPCParser::parseSignSubmit, 1, 3}, + {"submit", &RPCParser::parseSignSubmit, 1, 4}, {"submit_multisigned", &RPCParser::parseSubmitMultiSigned, 1, 1}, {"transaction_entry", &RPCParser::parseTransactionEntry, 2, 2}, {"tx", &RPCParser::parseTx, 1, 4}, diff --git a/src/xrpld/rpc/detail/TransactionSign.cpp b/src/xrpld/rpc/detail/TransactionSign.cpp index 175fd84c9b..aa7c706a19 100644 --- a/src/xrpld/rpc/detail/TransactionSign.cpp +++ b/src/xrpld/rpc/detail/TransactionSign.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -54,6 +55,7 @@ private: AccountID const* const multiSigningAcctID_; std::optional multiSignPublicKey_; Buffer multiSignature_; + std::optional> signatureTarget_; public: explicit SigningForParams() : multiSigningAcctID_(nullptr) @@ -116,12 +118,25 @@ public: return multiSignature_; } + std::optional> const& + getSignatureTarget() const + { + return signatureTarget_; + } + void setPublicKey(PublicKey const& multiSignPublicKey) { multiSignPublicKey_ = multiSignPublicKey; } + void + setSignatureTarget( + std::optional> const& field) + { + signatureTarget_ = field; + } + void moveMultiSignature(Buffer&& multiSignature) { @@ -427,6 +442,29 @@ transactionPreProcessImpl( bool const verify = !(params.isMember(jss::offline) && params[jss::offline].asBool()); + auto const signatureTarget = + [¶ms]() -> std::optional> { + if (params.isMember(jss::signature_target)) + return SField::getField(params[jss::signature_target].asString()); + return std::nullopt; + }(); + + // Make sure the signature target field is valid, if specified, and save the + // template for use later + auto const signatureTemplate = signatureTarget + ? InnerObjectFormats::getInstance().findSOTemplateBySField( + *signatureTarget) + : nullptr; + if (signatureTarget) + { + if (!signatureTemplate) + { // Invalid target field + return RPC::make_error( + rpcINVALID_PARAMS, signatureTarget->get().getName()); + } + signingArgs.setSignatureTarget(signatureTarget); + } + if (!params.isMember(jss::tx_json)) return RPC::missing_field_error(jss::tx_json); @@ -541,9 +579,10 @@ transactionPreProcessImpl( JLOG(j.trace()) << "verify: " << toBase58(calcAccountID(pk)) << " : " << toBase58(srcAddressID); - // Don't do this test if multisigning since the account and secret - // probably don't belong together in that case. - if (!signingArgs.isMultiSigning()) + // Don't do this test if multisigning or if the signature is going into + // an alternate field since the account and secret probably don't belong + // together in that case. + if (!signingArgs.isMultiSigning() && !signatureTarget) { // Make sure the account and secret belong together. if (tx_json.isMember(sfDelegate.jsonName)) @@ -598,7 +637,17 @@ transactionPreProcessImpl( { // If we're generating a multi-signature the SigningPubKey must be // empty, otherwise it must be the master account's public key. - parsed.object->setFieldVL( + STObject* sigObject = &*parsed.object; + if (signatureTarget) + { + // If the target object doesn't exist, make one. + if (!parsed.object->isFieldPresent(*signatureTarget)) + parsed.object->setFieldObject( + *signatureTarget, + STObject{*signatureTemplate, *signatureTarget}); + sigObject = &parsed.object->peekFieldObject(*signatureTarget); + } + sigObject->setFieldVL( sfSigningPubKey, signingArgs.isMultiSigning() ? Slice(nullptr, 0) : pk.slice()); @@ -630,7 +679,7 @@ transactionPreProcessImpl( } else if (signingArgs.isSingleSigning()) { - stTx->sign(pk, sk); + stTx->sign(pk, sk, signatureTarget); } return transactionPreProcessResult{std::move(stTx)}; @@ -1195,11 +1244,17 @@ transactionSignFor( signer.setFieldVL( sfSigningPubKey, signForParams.getPublicKey().slice()); + STObject& sigTarget = [&]() -> STObject& { + auto const target = signForParams.getSignatureTarget(); + if (target) + return sttx->peekFieldObject(*target); + return *sttx; + }(); // If there is not yet a Signers array, make one. - if (!sttx->isFieldPresent(sfSigners)) - sttx->setFieldArray(sfSigners, {}); + if (!sigTarget.isFieldPresent(sfSigners)) + sigTarget.setFieldArray(sfSigners, {}); - auto& signers = sttx->peekFieldArray(sfSigners); + auto& signers = sigTarget.peekFieldArray(sfSigners); signers.emplace_back(std::move(signer)); // The array must be sorted and validated.