diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 36145479e1..05b65edbfd 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -101,6 +101,7 @@ jobs: echo 'CMake arguments: ${{ matrix.cmake_args }}' echo 'CMake target: ${{ matrix.cmake_target }}' echo 'Config name: ${{ matrix.config_name }}' + - name: Clean workspace (MacOS) if: ${{ inputs.os == 'macos' }} run: | @@ -111,18 +112,12 @@ jobs: exit 1 fi find "${WORKSPACE}" -depth 1 | xargs rm -rfv + - name: Checkout repository uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - - name: Set up Python (Windows) - if: ${{ inputs.os == 'windows' }} - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 - with: - python-version: 3.13 - - name: Install build tools (Windows) - if: ${{ inputs.os == 'windows' }} - run: | - echo 'Installing build tools.' - pip install wheel conan + - name: Prepare runner + uses: XRPLF/actions/.github/actions/prepare-runner@638e0dc11ea230f91bd26622fb542116bb5254d5 + - name: Check configuration (Windows) if: ${{ inputs.os == 'windows' }} run: | @@ -134,11 +129,6 @@ jobs: echo 'Checking Conan version.' conan --version - - name: Install build tools (MacOS) - if: ${{ inputs.os == 'macos' }} - run: | - echo 'Installing build tools.' - brew install --quiet cmake conan ninja coreutils - name: Check configuration (Linux and MacOS) if: ${{ inputs.os == 'linux' || inputs.os == 'macos' }} run: | @@ -162,18 +152,7 @@ jobs: echo 'Checking nproc version.' nproc --version - - name: Set up Conan home directory (MacOS) - if: ${{ inputs.os == 'macos' }} - run: | - echo 'Setting up Conan home directory.' - export CONAN_HOME=${{ github.workspace }}/.conan - mkdir -p ${CONAN_HOME} - - name: Set up Conan home directory (Windows) - if: ${{ inputs.os == 'windows' }} - run: | - echo 'Setting up Conan home directory.' - set CONAN_HOME=${{ github.workspace }}\.conan - mkdir -p %CONAN_HOME% + - name: Set up Conan configuration run: | echo 'Installing configuration.' @@ -196,6 +175,7 @@ jobs: echo 'Listing Conan remotes.' conan remote list + - name: Build dependencies uses: ./.github/actions/build-deps with: diff --git a/.github/workflows/on-pr.yml b/.github/workflows/on-pr.yml index a5f1d60c42..a4bbfd0997 100644 --- a/.github/workflows/on-pr.yml +++ b/.github/workflows/on-pr.yml @@ -28,30 +28,26 @@ env: CONAN_REMOTE_URL: https://conan.ripplex.io jobs: - # This job determines whether the workflow should run. It runs when the PR is - # not a draft or has the 'DraftRunCI' label. + # This job determines whether the rest of the workflow should run. It runs + # when the PR is not a draft (which should also cover merge-group) or + # has the 'DraftRunCI' label. should-run: if: ${{ !github.event.pull_request.draft || contains(github.event.pull_request.labels.*.name, 'DraftRunCI') }} runs-on: ubuntu-latest - steps: - - name: No-op - run: true - - # This job checks whether any files have changed that should cause the next - # jobs to run. We do it this way rather than using `paths` in the `on:` - # section, because all required checks must pass, even for changes that do not - # modify anything that affects those checks. We would therefore like to make - # the checks required only if the job runs, but GitHub does not support that - # directly. By always executing the workflow on new commits and by using the - # changed-files action below, we ensure that Github considers any skipped jobs - # to have passed, and in turn the required checks as well. - any-changed: - needs: should-run - runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - name: Determine changed files + # This step checks whether any files have changed that should + # cause the next jobs to run. We do it this way rather than + # using `paths` in the `on:` section, because all required + # checks must pass, even for changes that do not modify anything + # that affects those checks. We would therefore like to make the + # checks required only if the job runs, but GitHub does not + # support that directly. By always executing the workflow on new + # commits and by using the changed-files action below, we ensure + # that Github considers any skipped jobs to have passed, and in + # turn the required checks as well. id: changes uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5 with: @@ -79,24 +75,40 @@ jobs: tests/** CMakeLists.txt conanfile.py + - name: Check whether to run + # This step determines whether the rest of the workflow should + # run. The rest of the workflow will run if this job runs AND at + # least one of: + # * Any of the files checked in the `changes` step were modified + # * The PR is NOT a draft and is labeled "Ready to merge" + # * The workflow is running from the merge queue + id: go + env: + FILES: ${{ steps.changes.outputs.any_changed }} + DRAFT: ${{ github.event.pull_request.draft }} + READY: ${{ contains(github.event.pull_request.labels.*.name, 'Ready to merge') }} + MERGE: ${{ github.event_name == 'merge_group' }} + run: | + echo "go=${{ (env.DRAFT != 'true' && env.READY == 'true') || env.FILES == 'true' || env.MERGE == 'true' }}" >> "${GITHUB_OUTPUT}" + cat "${GITHUB_OUTPUT}" outputs: - changed: ${{ steps.changes.outputs.any_changed }} + go: ${{ steps.go.outputs.go == 'true' }} check-format: - needs: any-changed - if: needs.any-changed.outputs.changed == 'true' + needs: should-run + if: needs.should-run.outputs.go == 'true' uses: ./.github/workflows/check-format.yml check-levelization: - needs: any-changed - if: needs.any-changed.outputs.changed == 'true' + needs: should-run + if: needs.should-run.outputs.go == 'true' uses: ./.github/workflows/check-levelization.yml # This job works around the limitation that GitHub Actions does not support # using environment variables as inputs for reusable workflows. generate-outputs: - needs: any-changed - if: needs.any-changed.outputs.changed == 'true' + needs: should-run + if: needs.should-run.outputs.go == 'true' runs-on: ubuntu-latest steps: - name: No-op @@ -130,3 +142,13 @@ jobs: clio_notify_token: ${{ secrets.CLIO_NOTIFY_TOKEN }} conan_remote_username: ${{ secrets.CONAN_REMOTE_USERNAME }} conan_remote_password: ${{ secrets.CONAN_REMOTE_PASSWORD }} + + passed: + needs: + - build-test + - check-format + - check-levelization + runs-on: ubuntu-latest + steps: + - name: No-op + run: true diff --git a/cmake/deps/Boost.cmake b/cmake/deps/Boost.cmake index bde40c0ce5..e431e57b0c 100644 --- a/cmake/deps/Boost.cmake +++ b/cmake/deps/Boost.cmake @@ -14,12 +14,6 @@ find_package(Boost 1.82 REQUIRED add_library(ripple_boost INTERFACE) add_library(Ripple::boost ALIAS ripple_boost) -if(XCODE) - target_include_directories(ripple_boost BEFORE INTERFACE ${Boost_INCLUDE_DIRS}) - target_compile_options(ripple_boost INTERFACE --system-header-prefix="boost/") -else() - target_include_directories(ripple_boost SYSTEM BEFORE INTERFACE ${Boost_INCLUDE_DIRS}) -endif() target_link_libraries(ripple_boost INTERFACE diff --git a/include/xrpl/protocol/ErrorCodes.h b/include/xrpl/protocol/ErrorCodes.h index f06b927566..5da3ad0b33 100644 --- a/include/xrpl/protocol/ErrorCodes.h +++ b/include/xrpl/protocol/ErrorCodes.h @@ -157,7 +157,12 @@ enum error_code_i { // Pathfinding rpcDOMAIN_MALFORMED = 97, - rpcLAST = rpcDOMAIN_MALFORMED // rpcLAST should always equal the last code. + // ledger_entry + rpcENTRY_NOT_FOUND = 98, + rpcUNEXPECTED_LEDGER_TYPE = 99, + + rpcLAST = + rpcUNEXPECTED_LEDGER_TYPE // rpcLAST should always equal the last code. }; /** Codes returned in the `warnings` array of certain RPC commands. diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index b4397f8174..5dbb6bfbf7 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -68,9 +68,13 @@ JSS(Flags); // in/out: TransactionSign; field. JSS(Holder); // field. JSS(Invalid); // JSS(Issuer); // in: Credential transactions +JSS(IssuingChainDoor); // field. +JSS(IssuingChainIssue); // field. JSS(LastLedgerSequence); // in: TransactionSign; field JSS(LastUpdateTime); // field. JSS(LimitAmount); // field. +JSS(LockingChainDoor); // field. +JSS(LockingChainIssue); // field. JSS(NetworkID); // field. JSS(LPTokenOut); // in: AMM Liquidity Provider deposit tokens JSS(LPTokenIn); // in: AMM Liquidity Provider withdraw tokens diff --git a/src/libxrpl/json/json_value.cpp b/src/libxrpl/json/json_value.cpp index a1e0a04875..1df8f6cf31 100644 --- a/src/libxrpl/json/json_value.cpp +++ b/src/libxrpl/json/json_value.cpp @@ -24,6 +24,7 @@ #include #include +#include #include #include #include @@ -685,7 +686,9 @@ Value::isConvertibleTo(ValueType other) const (other == intValue && value_.real_ >= minInt && value_.real_ <= maxInt) || (other == uintValue && value_.real_ >= 0 && - value_.real_ <= maxUInt) || + value_.real_ <= maxUInt && + std::fabs(round(value_.real_) - value_.real_) < + std::numeric_limits::epsilon()) || other == realValue || other == stringValue || other == booleanValue; diff --git a/src/libxrpl/protocol/BuildInfo.cpp b/src/libxrpl/protocol/BuildInfo.cpp index 4b55f82f49..d5077aa44d 100644 --- a/src/libxrpl/protocol/BuildInfo.cpp +++ b/src/libxrpl/protocol/BuildInfo.cpp @@ -36,7 +36,7 @@ namespace BuildInfo { // and follow the format described at http://semver.org/ //------------------------------------------------------------------------------ // clang-format off -char const* const versionString = "2.6.0-rc3" +char const* const versionString = "2.6.0" // clang-format on #if defined(DEBUG) || defined(SANITIZER) diff --git a/src/libxrpl/protocol/ErrorCodes.cpp b/src/libxrpl/protocol/ErrorCodes.cpp index 3109f51d05..ec295343ce 100644 --- a/src/libxrpl/protocol/ErrorCodes.cpp +++ b/src/libxrpl/protocol/ErrorCodes.cpp @@ -117,7 +117,10 @@ constexpr static ErrorInfo unorderedErrorInfos[]{ {rpcORACLE_MALFORMED, "oracleMalformed", "Oracle request is malformed.", 400}, {rpcBAD_CREDENTIALS, "badCredentials", "Credentials do not exist, are not accepted, or have expired.", 400}, {rpcTX_SIGNED, "transactionSigned", "Transaction should not be signed.", 400}, - {rpcDOMAIN_MALFORMED, "domainMalformed", "Domain is malformed.", 400}}; + {rpcDOMAIN_MALFORMED, "domainMalformed", "Domain is malformed.", 400}, + {rpcENTRY_NOT_FOUND, "entryNotFound", "Entry not found.", 400}, + {rpcUNEXPECTED_LEDGER_TYPE, "unexpectedLedgerType", "Unexpected ledger type.", 400}, +}; // clang-format on // Sort and validate unorderedErrorInfos at compile time. Should be diff --git a/src/libxrpl/protocol/STXChainBridge.cpp b/src/libxrpl/protocol/STXChainBridge.cpp index fb192d82d6..e835735f08 100644 --- a/src/libxrpl/protocol/STXChainBridge.cpp +++ b/src/libxrpl/protocol/STXChainBridge.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include @@ -98,12 +99,10 @@ STXChainBridge::STXChainBridge(SField const& name, Json::Value const& v) }; checkExtra(v); - Json::Value const& lockingChainDoorStr = - v[sfLockingChainDoor.getJsonName()]; - Json::Value const& lockingChainIssue = v[sfLockingChainIssue.getJsonName()]; - Json::Value const& issuingChainDoorStr = - v[sfIssuingChainDoor.getJsonName()]; - Json::Value const& issuingChainIssue = v[sfIssuingChainIssue.getJsonName()]; + Json::Value const& lockingChainDoorStr = v[jss::LockingChainDoor]; + Json::Value const& lockingChainIssue = v[jss::LockingChainIssue]; + Json::Value const& issuingChainDoorStr = v[jss::IssuingChainDoor]; + Json::Value const& issuingChainIssue = v[jss::IssuingChainIssue]; if (!lockingChainDoorStr.isString()) { @@ -161,10 +160,10 @@ Json::Value STXChainBridge::getJson(JsonOptions jo) const { Json::Value v; - v[sfLockingChainDoor.getJsonName()] = lockingChainDoor_.getJson(jo); - v[sfLockingChainIssue.getJsonName()] = lockingChainIssue_.getJson(jo); - v[sfIssuingChainDoor.getJsonName()] = issuingChainDoor_.getJson(jo); - v[sfIssuingChainIssue.getJsonName()] = issuingChainIssue_.getJson(jo); + v[jss::LockingChainDoor] = lockingChainDoor_.getJson(jo); + v[jss::LockingChainIssue] = lockingChainIssue_.getJson(jo); + v[jss::IssuingChainDoor] = issuingChainDoor_.getJson(jo); + v[jss::IssuingChainIssue] = issuingChainIssue_.getJson(jo); return v; } diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 7ea38db2b1..7add8b3eda 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -3028,18 +3028,6 @@ class Vault_test : public beast::unit_test::suite "malformedRequest"); } - { - testcase("RPC ledger_entry zero seq"); - Json::Value jvParams; - jvParams[jss::ledger_index] = jss::validated; - jvParams[jss::vault][jss::owner] = issuer.human(); - jvParams[jss::vault][jss::seq] = 0; - auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams)); - BEAST_EXPECT( - jvVault[jss::result][jss::error].asString() == - "malformedRequest"); - } - { testcase("RPC ledger_entry negative seq"); Json::Value jvParams; diff --git a/src/test/jtx/impl/xchain_bridge.cpp b/src/test/jtx/impl/xchain_bridge.cpp index 6f167d7508..9e8fa4795f 100644 --- a/src/test/jtx/impl/xchain_bridge.cpp +++ b/src/test/jtx/impl/xchain_bridge.cpp @@ -44,10 +44,10 @@ bridge( Issue const& issuingChainIssue) { Json::Value jv; - jv[sfLockingChainDoor.getJsonName()] = lockingChainDoor.human(); - jv[sfLockingChainIssue.getJsonName()] = to_json(lockingChainIssue); - jv[sfIssuingChainDoor.getJsonName()] = issuingChainDoor.human(); - jv[sfIssuingChainIssue.getJsonName()] = to_json(issuingChainIssue); + jv[jss::LockingChainDoor] = lockingChainDoor.human(); + jv[jss::LockingChainIssue] = to_json(lockingChainIssue); + jv[jss::IssuingChainDoor] = issuingChainDoor.human(); + jv[jss::IssuingChainIssue] = to_json(issuingChainIssue); return jv; } @@ -60,10 +60,10 @@ bridge_rpc( Issue const& issuingChainIssue) { Json::Value jv; - jv[sfLockingChainDoor.getJsonName()] = lockingChainDoor.human(); - jv[sfLockingChainIssue.getJsonName()] = to_json(lockingChainIssue); - jv[sfIssuingChainDoor.getJsonName()] = issuingChainDoor.human(); - jv[sfIssuingChainIssue.getJsonName()] = to_json(issuingChainIssue); + jv[jss::LockingChainDoor] = lockingChainDoor.human(); + jv[jss::LockingChainIssue] = to_json(lockingChainIssue); + jv[jss::IssuingChainDoor] = issuingChainDoor.human(); + jv[jss::IssuingChainIssue] = to_json(issuingChainIssue); return jv; } diff --git a/src/test/overlay/tx_reduce_relay_test.cpp b/src/test/overlay/tx_reduce_relay_test.cpp index 0c67fd581c..83b3013514 100644 --- a/src/test/overlay/tx_reduce_relay_test.cpp +++ b/src/test/overlay/tx_reduce_relay_test.cpp @@ -183,7 +183,7 @@ private: boost::asio::ip::make_address("172.1.1." + std::to_string(rid_))); PublicKey key(std::get<0>(randomKeyPair(KeyType::ed25519))); auto consumer = overlay.resourceManager().newInboundEndpoint(remote); - auto slot = overlay.peerFinder().new_inbound_slot(local, remote); + auto [slot, _] = overlay.peerFinder().new_inbound_slot(local, remote); auto const peer = std::make_shared( env.app(), slot, diff --git a/src/test/peerfinder/PeerFinder_test.cpp b/src/test/peerfinder/PeerFinder_test.cpp index f35cbbdaae..64a1eb5091 100644 --- a/src/test/peerfinder/PeerFinder_test.cpp +++ b/src/test/peerfinder/PeerFinder_test.cpp @@ -20,6 +20,7 @@ #include #include +#include #include #include @@ -98,7 +99,7 @@ public: if (!list.empty()) { BEAST_EXPECT(list.size() == 1); - auto const slot = logic.new_outbound_slot(list.front()); + auto const [slot, _] = logic.new_outbound_slot(list.front()); BEAST_EXPECT(logic.onConnected( slot, beast::IP::Endpoint::from_string("65.0.0.2:5"))); logic.on_closed(slot); @@ -139,7 +140,7 @@ public: if (!list.empty()) { BEAST_EXPECT(list.size() == 1); - auto const slot = logic.new_outbound_slot(list.front()); + auto const [slot, _] = logic.new_outbound_slot(list.front()); if (!BEAST_EXPECT(logic.onConnected( slot, beast::IP::Endpoint::from_string("65.0.0.2:5")))) return; @@ -158,6 +159,7 @@ public: BEAST_EXPECT(n <= (seconds + 59) / 60); } + // test accepting an incoming slot for an already existing outgoing slot void test_duplicateOutIn() { @@ -166,8 +168,6 @@ public: TestChecker checker; TestStopwatch clock; Logic logic(clock, store, checker, journal_); - logic.addFixedPeer( - "test", beast::IP::Endpoint::from_string("65.0.0.1:5")); { Config c; c.autoConnect = false; @@ -176,28 +176,24 @@ public: logic.config(c); } - auto const list = logic.autoconnect(); - if (BEAST_EXPECT(!list.empty())) - { - BEAST_EXPECT(list.size() == 1); - auto const remote = list.front(); - auto const slot1 = logic.new_outbound_slot(remote); - if (BEAST_EXPECT(slot1 != nullptr)) - { - BEAST_EXPECT( - logic.connectedAddresses_.count(remote.address()) == 1); - auto const local = - beast::IP::Endpoint::from_string("65.0.0.2:1024"); - auto const slot2 = logic.new_inbound_slot(local, remote); - BEAST_EXPECT( - logic.connectedAddresses_.count(remote.address()) == 1); - if (!BEAST_EXPECT(slot2 == nullptr)) - logic.on_closed(slot2); - logic.on_closed(slot1); - } - } + auto const remote = beast::IP::Endpoint::from_string("65.0.0.1:5"); + auto const [slot1, r] = logic.new_outbound_slot(remote); + BEAST_EXPECT(slot1 != nullptr); + BEAST_EXPECT(r == Result::success); + BEAST_EXPECT(logic.connectedAddresses_.count(remote.address()) == 1); + + auto const local = beast::IP::Endpoint::from_string("65.0.0.2:1024"); + auto const [slot2, r2] = logic.new_inbound_slot(local, remote); + BEAST_EXPECT(logic.connectedAddresses_.count(remote.address()) == 1); + BEAST_EXPECT(r2 == Result::duplicatePeer); + + if (!BEAST_EXPECT(slot2 == nullptr)) + logic.on_closed(slot2); + + logic.on_closed(slot1); } + // test establishing outgoing slot for an already existing incoming slot void test_duplicateInOut() { @@ -206,8 +202,6 @@ public: TestChecker checker; TestStopwatch clock; Logic logic(clock, store, checker, journal_); - logic.addFixedPeer( - "test", beast::IP::Endpoint::from_string("65.0.0.1:5")); { Config c; c.autoConnect = false; @@ -216,33 +210,202 @@ public: logic.config(c); } - auto const list = logic.autoconnect(); - if (BEAST_EXPECT(!list.empty())) + auto const remote = beast::IP::Endpoint::from_string("65.0.0.1:5"); + auto const local = beast::IP::Endpoint::from_string("65.0.0.2:1024"); + + auto const [slot1, r] = logic.new_inbound_slot(local, remote); + BEAST_EXPECT(slot1 != nullptr); + BEAST_EXPECT(r == Result::success); + BEAST_EXPECT(logic.connectedAddresses_.count(remote.address()) == 1); + + auto const [slot2, r2] = logic.new_outbound_slot(remote); + BEAST_EXPECT(r2 == Result::duplicatePeer); + BEAST_EXPECT(logic.connectedAddresses_.count(remote.address()) == 1); + if (!BEAST_EXPECT(slot2 == nullptr)) + logic.on_closed(slot2); + logic.on_closed(slot1); + } + + void + test_peerLimitExceeded() + { + testcase("peer limit exceeded"); + TestStore store; + TestChecker checker; + TestStopwatch clock; + Logic logic(clock, store, checker, journal_); { - BEAST_EXPECT(list.size() == 1); - auto const remote = list.front(); - auto const local = - beast::IP::Endpoint::from_string("65.0.0.2:1024"); - auto const slot1 = logic.new_inbound_slot(local, remote); - if (BEAST_EXPECT(slot1 != nullptr)) - { - BEAST_EXPECT( - logic.connectedAddresses_.count(remote.address()) == 1); - auto const slot2 = logic.new_outbound_slot(remote); - BEAST_EXPECT( - logic.connectedAddresses_.count(remote.address()) == 1); - if (!BEAST_EXPECT(slot2 == nullptr)) - logic.on_closed(slot2); - logic.on_closed(slot1); - } + Config c; + c.autoConnect = false; + c.listeningPort = 1024; + c.ipLimit = 2; + logic.config(c); } + + auto const local = beast::IP::Endpoint::from_string("65.0.0.2:1024"); + auto const [slot, r] = logic.new_inbound_slot( + local, beast::IP::Endpoint::from_string("55.104.0.2:1025")); + BEAST_EXPECT(slot != nullptr); + BEAST_EXPECT(r == Result::success); + + auto const [slot1, r1] = logic.new_inbound_slot( + local, beast::IP::Endpoint::from_string("55.104.0.2:1026")); + BEAST_EXPECT(slot1 != nullptr); + BEAST_EXPECT(r1 == Result::success); + + auto const [slot2, r2] = logic.new_inbound_slot( + local, beast::IP::Endpoint::from_string("55.104.0.2:1027")); + BEAST_EXPECT(r2 == Result::ipLimitExceeded); + + if (!BEAST_EXPECT(slot2 == nullptr)) + logic.on_closed(slot2); + logic.on_closed(slot1); + logic.on_closed(slot); + } + + void + test_activate_duplicate_peer() + { + testcase("test activate duplicate peer"); + TestStore store; + TestChecker checker; + TestStopwatch clock; + Logic logic(clock, store, checker, journal_); + { + Config c; + c.autoConnect = false; + c.listeningPort = 1024; + c.ipLimit = 2; + logic.config(c); + } + + auto const local = beast::IP::Endpoint::from_string("65.0.0.2:1024"); + + PublicKey const pk1(randomKeyPair(KeyType::secp256k1).first); + + auto const [slot, rSlot] = logic.new_outbound_slot( + beast::IP::Endpoint::from_string("55.104.0.2:1025")); + BEAST_EXPECT(slot != nullptr); + BEAST_EXPECT(rSlot == Result::success); + + auto const [slot2, r2Slot] = logic.new_outbound_slot( + beast::IP::Endpoint::from_string("55.104.0.2:1026")); + BEAST_EXPECT(slot2 != nullptr); + BEAST_EXPECT(r2Slot == Result::success); + + BEAST_EXPECT(logic.onConnected(slot, local)); + BEAST_EXPECT(logic.onConnected(slot2, local)); + + BEAST_EXPECT(logic.activate(slot, pk1, false) == Result::success); + + // activating a different slot with the same node ID (pk) must fail + BEAST_EXPECT( + logic.activate(slot2, pk1, false) == Result::duplicatePeer); + + logic.on_closed(slot); + + // accept the same key for a new slot after removing the old slot + BEAST_EXPECT(logic.activate(slot2, pk1, false) == Result::success); + logic.on_closed(slot2); + } + + void + test_activate_inbound_disabled() + { + testcase("test activate inbound disabled"); + TestStore store; + TestChecker checker; + TestStopwatch clock; + Logic logic(clock, store, checker, journal_); + { + Config c; + c.autoConnect = false; + c.listeningPort = 1024; + c.ipLimit = 2; + logic.config(c); + } + + PublicKey const pk1(randomKeyPair(KeyType::secp256k1).first); + auto const local = beast::IP::Endpoint::from_string("65.0.0.2:1024"); + + auto const [slot, rSlot] = logic.new_inbound_slot( + local, beast::IP::Endpoint::from_string("55.104.0.2:1025")); + BEAST_EXPECT(slot != nullptr); + BEAST_EXPECT(rSlot == Result::success); + + BEAST_EXPECT( + logic.activate(slot, pk1, false) == Result::inboundDisabled); + + { + Config c; + c.autoConnect = false; + c.listeningPort = 1024; + c.ipLimit = 2; + c.inPeers = 1; + logic.config(c); + } + // new inbound slot must succeed when inbound connections are enabled + BEAST_EXPECT(logic.activate(slot, pk1, false) == Result::success); + + // creating a new inbound slot must succeed as IP Limit is not exceeded + auto const [slot2, r2Slot] = logic.new_inbound_slot( + local, beast::IP::Endpoint::from_string("55.104.0.2:1026")); + BEAST_EXPECT(slot2 != nullptr); + BEAST_EXPECT(r2Slot == Result::success); + + PublicKey const pk2(randomKeyPair(KeyType::secp256k1).first); + + // an inbound slot exceeding inPeers limit must fail + BEAST_EXPECT(logic.activate(slot2, pk2, false) == Result::full); + + logic.on_closed(slot2); + logic.on_closed(slot); + } + + void + test_addFixedPeer_no_port() + { + testcase("test addFixedPeer no port"); + TestStore store; + TestChecker checker; + TestStopwatch clock; + Logic logic(clock, store, checker, journal_); + try + { + logic.addFixedPeer( + "test", beast::IP::Endpoint::from_string("65.0.0.2")); + fail("invalid endpoint successfully added"); + } + catch (std::runtime_error const& e) + { + pass(); + } + } + + void + test_onConnected_self_connection() + { + testcase("test onConnected self connection"); + TestStore store; + TestChecker checker; + TestStopwatch clock; + Logic logic(clock, store, checker, journal_); + + auto const local = beast::IP::Endpoint::from_string("65.0.0.2:1234"); + auto const [slot, r] = logic.new_outbound_slot(local); + BEAST_EXPECT(slot != nullptr); + BEAST_EXPECT(r == Result::success); + + // Must fail when a slot is to our own IP address + BEAST_EXPECT(!logic.onConnected(slot, local)); + logic.on_closed(slot); } void test_config() { - // if peers_max is configured then peers_in_max and peers_out_max are - // ignored + // if peers_max is configured then peers_in_max and peers_out_max + // are ignored auto run = [&](std::string const& test, std::optional maxPeers, std::optional maxIn, @@ -282,13 +445,21 @@ public: Counts counts; counts.onConfig(config); BEAST_EXPECT( - counts.out_max() == expectOut && - counts.inboundSlots() == expectIn && + counts.out_max() == expectOut && counts.in_max() == expectIn && config.ipLimit == expectIpLimit); + + TestStore store; + TestChecker checker; + TestStopwatch clock; + Logic logic(clock, store, checker, journal_); + logic.config(config); + + BEAST_EXPECT(logic.config() == config); }; // if max_peers == 0 => maxPeers = 21, - // else if max_peers < 10 => maxPeers = 10 else maxPeers = max_peers + // else if max_peers < 10 => maxPeers = 10 else maxPeers = + // max_peers // expectOut => if legacy => max(0.15 * maxPeers, 10), // if legacy && !wantIncoming => maxPeers else max_out_peers // expectIn => if legacy && wantIncoming => maxPeers - outPeers @@ -364,6 +535,11 @@ public: test_duplicateInOut(); test_config(); test_invalid_config(); + test_peerLimitExceeded(); + test_activate_duplicate_peer(); + test_activate_inbound_disabled(); + test_addFixedPeer_no_port(); + test_onConnected_self_connection(); } }; diff --git a/src/test/rpc/LedgerEntry_test.cpp b/src/test/rpc/LedgerEntry_test.cpp index 89cb7b72eb..a88f6ab612 100644 --- a/src/test/rpc/LedgerEntry_test.cpp +++ b/src/test/rpc/LedgerEntry_test.cpp @@ -31,40 +31,435 @@ #include #include +#if (defined(__clang_major__) && __clang_major__ < 15) +#include +using source_location = std::experimental::source_location; +#else +#include +using std::source_location; +#endif namespace ripple { namespace test { +enum class FieldType { + AccountField, + BlobField, + ArrayField, + CurrencyField, + HashField, + HashOrObjectField, + ObjectField, + StringField, + TwoAccountArrayField, + UInt32Field, + UInt64Field, +}; + +std::vector> mappings{ + {jss::account, FieldType::AccountField}, + {jss::accounts, FieldType::TwoAccountArrayField}, + {jss::authorize, FieldType::AccountField}, + {jss::authorized, FieldType::AccountField}, + {jss::credential_type, FieldType::BlobField}, + {jss::currency, FieldType::CurrencyField}, + {jss::issuer, FieldType::AccountField}, + {jss::oracle_document_id, FieldType::UInt32Field}, + {jss::owner, FieldType::AccountField}, + {jss::seq, FieldType::UInt32Field}, + {jss::subject, FieldType::AccountField}, + {jss::ticket_seq, FieldType::UInt32Field}, +}; + +FieldType +getFieldType(Json::StaticString fieldName) +{ + auto it = std::ranges::find_if(mappings, [&fieldName](auto const& pair) { + return pair.first == fieldName; + }); + if (it != mappings.end()) + { + return it->second; + } + else + { + Throw( + "`mappings` is missing field " + std::string(fieldName.c_str())); + } +} + +std::string +getTypeName(FieldType typeID) +{ + switch (typeID) + { + case FieldType::UInt32Field: + return "number"; + case FieldType::UInt64Field: + return "number"; + case FieldType::HashField: + return "hex string"; + case FieldType::AccountField: + return "AccountID"; + case FieldType::BlobField: + return "hex string"; + case FieldType::CurrencyField: + return "Currency"; + case FieldType::ArrayField: + return "array"; + case FieldType::HashOrObjectField: + return "hex string or object"; + case FieldType::TwoAccountArrayField: + return "length-2 array of Accounts"; + default: + Throw( + "unknown type " + std::to_string(static_cast(typeID))); + } +} + class LedgerEntry_test : public beast::unit_test::suite { void checkErrorValue( Json::Value const& jv, std::string const& err, - std::string const& msg) + std::string const& msg, + source_location const location = source_location::current()) { if (BEAST_EXPECT(jv.isMember(jss::status))) - BEAST_EXPECT(jv[jss::status] == "error"); + BEAST_EXPECTS( + jv[jss::status] == "error", std::to_string(location.line())); if (BEAST_EXPECT(jv.isMember(jss::error))) - BEAST_EXPECT(jv[jss::error] == err); + BEAST_EXPECTS( + jv[jss::error] == err, + "Expected error " + err + ", received " + + jv[jss::error].asString() + ", at line " + + std::to_string(location.line()) + ", " + + jv.toStyledString()); if (msg.empty()) { - BEAST_EXPECT( + BEAST_EXPECTS( jv[jss::error_message] == Json::nullValue || - jv[jss::error_message] == ""); + jv[jss::error_message] == "", + "Expected no error message, received \"" + + jv[jss::error_message].asString() + "\", at line " + + std::to_string(location.line()) + ", " + + jv.toStyledString()); } else if (BEAST_EXPECT(jv.isMember(jss::error_message))) - BEAST_EXPECT(jv[jss::error_message] == msg); + BEAST_EXPECTS( + jv[jss::error_message] == msg, + "Expected error message \"" + msg + "\", received \"" + + jv[jss::error_message].asString() + "\", at line " + + std::to_string(location.line()) + ", " + + jv.toStyledString()); } - // Corrupt a valid address by replacing the 10th character with '!'. - // '!' is not part of the ripple alphabet. - std::string - makeBadAddress(std::string good) + std::vector + getBadValues(FieldType fieldType) { - std::string ret = std::move(good); - ret.replace(10, 1, 1, '!'); - return ret; + static Json::Value const injectObject = []() { + Json::Value obj(Json::objectValue); + obj[jss::account] = "rhigTLJJyXXSRUyRCQtqi1NoAZZzZnS4KU"; + obj[jss::ledger_index] = "validated"; + return obj; + }(); + static Json::Value const injectArray = []() { + Json::Value arr(Json::arrayValue); + arr[0u] = "rhigTLJJyXXSRUyRCQtqi1NoAZZzZnS4KU"; + arr[1u] = "validated"; + return arr; + }(); + static std::array const allBadValues = { + "", // 0 + true, // 1 + 1, // 2 + "1", // 3 + -1, // 4 + 1.1, // 5 + "-1", // 6 + "abcdef", // 7 + "ABCDEF", // 8 + "12KK", // 9 + "0123456789ABCDEFGH", // 10 + "rJxKV9e9p6wiPw!!!!xrJ4X1n98LosPL1sgcJW", // 11 + "rPSTrR5yEr11uMkfsz1kHCp9jK4aoa3Avv", // 12 + "n9K2isxwTxcSHJKxMkJznDoWXAUs7NNy49H9Fknz1pC7oHAH3kH9", // 13 + "USD", // 14 + "USDollars", // 15 + "5233D68B4D44388F98559DE42903767803EFA7C1F8D01413FC16EE6B01403D" + "6D", // 16 + Json::arrayValue, // 17 + Json::objectValue, // 18 + injectObject, // 19 + injectArray // 20 + }; + + auto remove = + [&](std::vector indices) -> std::vector { + std::unordered_set indexSet( + indices.begin(), indices.end()); + std::vector values; + values.reserve(allBadValues.size() - indexSet.size()); + for (std::size_t i = 0; i < allBadValues.size(); ++i) + { + if (indexSet.find(i) == indexSet.end()) + { + values.push_back(allBadValues[i]); + } + } + return values; + }; + + static auto const& badUInt32Values = remove({2, 3}); + static auto const& badUInt64Values = remove({2, 3}); + static auto const& badHashValues = remove({2, 3, 7, 8, 16}); + static auto const& badAccountValues = remove({12}); + static auto const& badBlobValues = remove({3, 7, 8, 16}); + static auto const& badCurrencyValues = remove({14}); + static auto const& badArrayValues = remove({17, 20}); + static auto const& badIndexValues = remove({12, 16, 18, 19}); + + switch (fieldType) + { + case FieldType::UInt32Field: + return badUInt32Values; + case FieldType::UInt64Field: + return badUInt64Values; + case FieldType::HashField: + return badHashValues; + case FieldType::AccountField: + return badAccountValues; + case FieldType::BlobField: + return badBlobValues; + case FieldType::CurrencyField: + return badCurrencyValues; + case FieldType::ArrayField: + case FieldType::TwoAccountArrayField: + return badArrayValues; + case FieldType::HashOrObjectField: + return badIndexValues; + default: + Throw( + "unknown type " + + std::to_string(static_cast(fieldType))); + } + } + + Json::Value + getCorrectValue(Json::StaticString fieldName) + { + static Json::Value const twoAccountArray = []() { + Json::Value arr(Json::arrayValue); + arr[0u] = "rhigTLJJyXXSRUyRCQtqi1NoAZZzZnS4KU"; + arr[1u] = "r4MrUGTdB57duTnRs6KbsRGQXgkseGb1b5"; + return arr; + }(); + + auto const typeID = getFieldType(fieldName); + switch (typeID) + { + case FieldType::UInt32Field: + return 1; + case FieldType::UInt64Field: + return 1; + case FieldType::HashField: + return "5233D68B4D44388F98559DE42903767803EFA7C1F8D01413FC16EE6" + "B01403D6D"; + case FieldType::AccountField: + return "r4MrUGTdB57duTnRs6KbsRGQXgkseGb1b5"; + case FieldType::BlobField: + return "ABCDEF"; + case FieldType::CurrencyField: + return "USD"; + case FieldType::ArrayField: + return Json::arrayValue; + case FieldType::HashOrObjectField: + return "5233D68B4D44388F98559DE42903767803EFA7C1F8D01413FC16EE6" + "B01403D6D"; + case FieldType::TwoAccountArrayField: + return twoAccountArray; + default: + Throw( + "unknown type " + + std::to_string(static_cast(typeID))); + } + } + + void + testMalformedField( + test::jtx::Env& env, + Json::Value correctRequest, + Json::StaticString const fieldName, + FieldType const typeID, + std::string const& expectedError, + bool required = true, + source_location const location = source_location::current()) + { + forAllApiVersions([&, this](unsigned apiVersion) { + if (required) + { + correctRequest.removeMember(fieldName); + Json::Value const jrr = env.rpc( + apiVersion, + "json", + "ledger_entry", + to_string(correctRequest))[jss::result]; + if (apiVersion < 2u) + checkErrorValue(jrr, "unknownOption", "", location); + else + checkErrorValue( + jrr, + "invalidParams", + "No ledger_entry params provided.", + location); + } + auto tryField = [&](Json::Value fieldValue) -> void { + correctRequest[fieldName] = fieldValue; + Json::Value const jrr = env.rpc( + apiVersion, + "json", + "ledger_entry", + to_string(correctRequest))[jss::result]; + auto const expectedErrMsg = + RPC::expected_field_message(fieldName, getTypeName(typeID)); + checkErrorValue(jrr, expectedError, expectedErrMsg, location); + }; + + auto const& badValues = getBadValues(typeID); + for (auto const& value : badValues) + { + tryField(value); + } + if (required) + { + tryField(Json::nullValue); + } + }); + } + + void + testMalformedSubfield( + test::jtx::Env& env, + Json::Value correctRequest, + Json::StaticString parentFieldName, + Json::StaticString fieldName, + FieldType typeID, + std::string const& expectedError, + bool required = true, + source_location const location = source_location::current()) + { + forAllApiVersions([&, this](unsigned apiVersion) { + if (required) + { + correctRequest[parentFieldName].removeMember(fieldName); + Json::Value const jrr = env.rpc( + apiVersion, + "json", + "ledger_entry", + to_string(correctRequest))[jss::result]; + checkErrorValue( + jrr, + "malformedRequest", + RPC::missing_field_message(fieldName.c_str()), + location); + + correctRequest[parentFieldName][fieldName] = Json::nullValue; + Json::Value const jrr2 = env.rpc( + apiVersion, + "json", + "ledger_entry", + to_string(correctRequest))[jss::result]; + checkErrorValue( + jrr2, + "malformedRequest", + RPC::missing_field_message(fieldName.c_str()), + location); + } + auto tryField = [&](Json::Value fieldValue) -> void { + correctRequest[parentFieldName][fieldName] = fieldValue; + + Json::Value const jrr = env.rpc( + apiVersion, + "json", + "ledger_entry", + to_string(correctRequest))[jss::result]; + checkErrorValue( + jrr, + expectedError, + RPC::expected_field_message(fieldName, getTypeName(typeID)), + location); + }; + + auto const& badValues = getBadValues(typeID); + for (auto const& value : badValues) + { + tryField(value); + } + }); + } + + // No subfields + void + runLedgerEntryTest( + test::jtx::Env& env, + Json::StaticString const& parentField, + source_location const location = source_location::current()) + { + testMalformedField( + env, + Json::Value{}, + parentField, + FieldType::HashField, + "malformedRequest", + true, + location); + } + + struct Subfield + { + Json::StaticString fieldName; + std::string malformedErrorMsg; + bool required = true; + }; + + void + runLedgerEntryTest( + test::jtx::Env& env, + Json::StaticString const& parentField, + std::vector const& subfields, + source_location const location = source_location::current()) + { + testMalformedField( + env, + Json::Value{}, + parentField, + FieldType::HashOrObjectField, + "malformedRequest", + true, + location); + + Json::Value correctOutput; + correctOutput[parentField] = Json::objectValue; + for (auto const& subfield : subfields) + { + correctOutput[parentField][subfield.fieldName] = + getCorrectValue(subfield.fieldName); + } + + for (auto const& subfield : subfields) + { + auto const fieldType = getFieldType(subfield.fieldName); + testMalformedSubfield( + env, + correctOutput, + parentField, + subfield.fieldName, + fieldType, + subfield.malformedErrorMsg, + subfield.required, + location); + } } void @@ -76,7 +471,6 @@ class LedgerEntry_test : public beast::unit_test::suite Account const alice{"alice"}; env.fund(XRP(10000), alice); env.close(); - { // Missing ledger_entry ledger_hash Json::Value jvParams; @@ -88,6 +482,33 @@ class LedgerEntry_test : public beast::unit_test::suite "json", "ledger_entry", to_string(jvParams))[jss::result]; checkErrorValue(jrr, "lgrNotFound", "ledgerNotFound"); } + { + // Missing ledger_entry ledger_hash + Json::Value jvParams; + jvParams[jss::account_root] = alice.human(); + auto const typeId = FieldType::HashField; + + forAllApiVersions([&, this](unsigned apiVersion) { + auto tryField = [&](Json::Value fieldValue) -> void { + jvParams[jss::ledger_hash] = fieldValue; + Json::Value const jrr = env.rpc( + apiVersion, + "json", + "ledger_entry", + to_string(jvParams))[jss::result]; + auto const expectedErrMsg = fieldValue.isString() + ? "ledgerHashMalformed" + : "ledgerHashNotString"; + checkErrorValue(jrr, "invalidParams", expectedErrMsg); + }; + + auto const& badValues = getBadValues(typeId); + for (auto const& value : badValues) + { + tryField(value); + } + }); + } { // ask for an zero index @@ -95,17 +516,38 @@ class LedgerEntry_test : public beast::unit_test::suite jvParams[jss::ledger_index] = "validated"; jvParams[jss::index] = "00000000000000000000000000000000000000000000000000000000000000" - "0000"; + "00"; auto const jrr = env.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); + checkErrorValue(jrr, "entryNotFound", "Entry not found."); } + + forAllApiVersions([&, this](unsigned apiVersion) { + // "features" is not an option supported by ledger_entry. + { + Json::Value jvParams = Json::objectValue; + jvParams[jss::features] = + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAA"; + jvParams[jss::api_version] = apiVersion; + Json::Value const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + + if (apiVersion < 2u) + checkErrorValue(jrr, "unknownOption", ""); + else + checkErrorValue( + jrr, + "invalidParams", + "No ledger_entry params provided."); + } + }); } void testLedgerEntryAccountRoot() { - testcase("ledger_entry Request AccountRoot"); + testcase("AccountRoot"); using namespace test::jtx; auto cfg = envconfig(); @@ -176,13 +618,26 @@ class LedgerEntry_test : public beast::unit_test::suite BEAST_EXPECT(jrr[jss::node][sfBalance.jsonName] == "10000000000"); } { - // Request using a corrupted AccountID. + // Check alias Json::Value jvParams; - jvParams[jss::account_root] = makeBadAddress(alice.human()); + jvParams[jss::account] = alice.human(); jvParams[jss::ledger_hash] = ledgerHash; Json::Value const jrr = env.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedAddress", ""); + BEAST_EXPECT(jrr.isMember(jss::node)); + BEAST_EXPECT(jrr[jss::node][jss::Account] == alice.human()); + BEAST_EXPECT(jrr[jss::node][sfBalance.jsonName] == "10000000000"); + accountRootIndex = jrr[jss::index].asString(); + } + { + // Check malformed cases + Json::Value jvParams; + testMalformedField( + env, + jvParams, + jss::account_root, + FieldType::AccountField, + "malformedAddress"); } { // Request an account that is not in the ledger. @@ -191,14 +646,14 @@ class LedgerEntry_test : public beast::unit_test::suite jvParams[jss::ledger_hash] = ledgerHash; Json::Value const jrr = env.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "entryNotFound", ""); + checkErrorValue(jrr, "entryNotFound", "Entry not found."); } } void testLedgerEntryCheck() { - testcase("ledger_entry Request Check"); + testcase("Check"); using namespace test::jtx; Env env{*this}; Account const alice{"alice"}; @@ -238,14 +693,19 @@ class LedgerEntry_test : public beast::unit_test::suite jvParams[jss::ledger_hash] = ledgerHash; Json::Value const jrr = env.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "unexpectedLedgerType", ""); + checkErrorValue( + jrr, "unexpectedLedgerType", "Unexpected ledger type."); + } + { + // Check malformed cases + runLedgerEntryTest(env, jss::check); } } void testLedgerEntryCredentials() { - testcase("ledger_entry credentials"); + testcase("Credentials"); using namespace test::jtx; @@ -287,163 +747,33 @@ class LedgerEntry_test : public beast::unit_test::suite jss::Credential); } - { - // Fail, index not a hash - auto const jv = credentials::ledgerEntry(env, ""); - checkErrorValue(jv[jss::result], "malformedRequest", ""); - } - { // Fail, credential doesn't exist auto const jv = credentials::ledgerEntry( env, "48004829F915654A81B11C4AB8218D96FED67F209B58328A72314FB6EA288B" "E4"); - checkErrorValue(jv[jss::result], "entryNotFound", ""); + checkErrorValue( + jv[jss::result], "entryNotFound", "Entry not found."); } { - // Fail, invalid subject - Json::Value jv; - jv[jss::ledger_index] = jss::validated; - jv[jss::credential][jss::subject] = 42; - jv[jss::credential][jss::issuer] = issuer.human(); - jv[jss::credential][jss::credential_type] = - strHex(std::string_view(credType)); - auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); - checkErrorValue(jrr[jss::result], "malformedRequest", ""); - } - - { - // Fail, invalid issuer - Json::Value jv; - jv[jss::ledger_index] = jss::validated; - jv[jss::credential][jss::subject] = alice.human(); - jv[jss::credential][jss::issuer] = 42; - jv[jss::credential][jss::credential_type] = - strHex(std::string_view(credType)); - auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); - checkErrorValue(jrr[jss::result], "malformedRequest", ""); - } - - { - // Fail, invalid credentials type - Json::Value jv; - jv[jss::ledger_index] = jss::validated; - jv[jss::credential][jss::subject] = alice.human(); - jv[jss::credential][jss::issuer] = issuer.human(); - jv[jss::credential][jss::credential_type] = 42; - auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); - checkErrorValue(jrr[jss::result], "malformedRequest", ""); - } - - { - // Fail, empty subject - Json::Value jv; - jv[jss::ledger_index] = jss::validated; - jv[jss::credential][jss::subject] = ""; - jv[jss::credential][jss::issuer] = issuer.human(); - jv[jss::credential][jss::credential_type] = - strHex(std::string_view(credType)); - auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); - checkErrorValue(jrr[jss::result], "malformedRequest", ""); - } - - { - // Fail, empty issuer - Json::Value jv; - jv[jss::ledger_index] = jss::validated; - jv[jss::credential][jss::subject] = alice.human(); - jv[jss::credential][jss::issuer] = ""; - jv[jss::credential][jss::credential_type] = - strHex(std::string_view(credType)); - auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); - checkErrorValue(jrr[jss::result], "malformedRequest", ""); - } - - { - // Fail, empty credentials type - Json::Value jv; - jv[jss::ledger_index] = jss::validated; - jv[jss::credential][jss::subject] = alice.human(); - jv[jss::credential][jss::issuer] = issuer.human(); - jv[jss::credential][jss::credential_type] = ""; - auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); - checkErrorValue(jrr[jss::result], "malformedRequest", ""); - } - - { - // Fail, no subject - Json::Value jv; - jv[jss::ledger_index] = jss::validated; - jv[jss::credential][jss::issuer] = issuer.human(); - jv[jss::credential][jss::credential_type] = - strHex(std::string_view(credType)); - auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); - checkErrorValue(jrr[jss::result], "malformedRequest", ""); - } - - { - // Fail, no issuer - Json::Value jv; - jv[jss::ledger_index] = jss::validated; - jv[jss::credential][jss::subject] = alice.human(); - jv[jss::credential][jss::credential_type] = - strHex(std::string_view(credType)); - auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); - checkErrorValue(jrr[jss::result], "malformedRequest", ""); - } - - { - // Fail, no credentials type - Json::Value jv; - jv[jss::ledger_index] = jss::validated; - jv[jss::credential][jss::subject] = alice.human(); - jv[jss::credential][jss::issuer] = issuer.human(); - auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); - checkErrorValue(jrr[jss::result], "malformedRequest", ""); - } - - { - // Fail, not AccountID subject - Json::Value jv; - jv[jss::ledger_index] = jss::validated; - jv[jss::credential][jss::subject] = "wehsdbvasbdfvj"; - jv[jss::credential][jss::issuer] = issuer.human(); - jv[jss::credential][jss::credential_type] = - strHex(std::string_view(credType)); - auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); - checkErrorValue(jrr[jss::result], "malformedRequest", ""); - } - - { - // Fail, not AccountID issuer - Json::Value jv; - jv[jss::ledger_index] = jss::validated; - jv[jss::credential][jss::subject] = alice.human(); - jv[jss::credential][jss::issuer] = "c4p93ugndfbsiu"; - jv[jss::credential][jss::credential_type] = - strHex(std::string_view(credType)); - auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); - checkErrorValue(jrr[jss::result], "malformedRequest", ""); - } - - { - // Fail, credentials type isn't hex encoded - Json::Value jv; - jv[jss::ledger_index] = jss::validated; - jv[jss::credential][jss::subject] = alice.human(); - jv[jss::credential][jss::issuer] = issuer.human(); - jv[jss::credential][jss::credential_type] = "12KK"; - auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); - checkErrorValue(jrr[jss::result], "malformedRequest", ""); + // Check all malformed cases + runLedgerEntryTest( + env, + jss::credential, + { + {jss::subject, "malformedRequest"}, + {jss::issuer, "malformedRequest"}, + {jss::credential_type, "malformedRequest"}, + }); } } void testLedgerEntryDelegate() { - testcase("ledger_entry Delegate"); + testcase("Delegate"); using namespace test::jtx; @@ -482,78 +812,23 @@ class LedgerEntry_test : public beast::unit_test::suite BEAST_EXPECT(jrr[jss::node][sfAccount.jsonName] == alice.human()); BEAST_EXPECT(jrr[jss::node][sfAuthorize.jsonName] == bob.human()); } + { - // Malformed request: delegate neither object nor string. - Json::Value jvParams; - jvParams[jss::delegate] = 5; - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); - } - { - // Malformed request: delegate not hex string. - Json::Value jvParams; - jvParams[jss::delegate] = "0123456789ABCDEFG"; - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); - } - { - // Malformed request: account not a string - Json::Value jvParams; - jvParams[jss::delegate][jss::account] = 5; - jvParams[jss::delegate][jss::authorize] = bob.human(); - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedAddress", ""); - } - { - // Malformed request: authorize not a string - Json::Value jvParams; - jvParams[jss::delegate][jss::account] = alice.human(); - jvParams[jss::delegate][jss::authorize] = 5; - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedAddress", ""); - } - { - // this lambda function is used test malformed account and authroize - auto testMalformedAccount = - [&](std::optional const& account, - std::optional const& authorize, - std::string const& error) { - Json::Value jvParams; - jvParams[jss::ledger_hash] = ledgerHash; - if (account) - jvParams[jss::delegate][jss::account] = *account; - if (authorize) - jvParams[jss::delegate][jss::authorize] = *authorize; - auto const jrr = env.rpc( - "json", - "ledger_entry", - to_string(jvParams))[jss::result]; - checkErrorValue(jrr, error, ""); - }; - // missing account - testMalformedAccount(std::nullopt, bob.human(), "malformedRequest"); - // missing authorize - testMalformedAccount( - alice.human(), std::nullopt, "malformedRequest"); - // malformed account - testMalformedAccount("-", bob.human(), "malformedAddress"); - // malformed authorize - testMalformedAccount(alice.human(), "-", "malformedAddress"); + // Check all malformed cases + runLedgerEntryTest( + env, + jss::delegate, + { + {jss::account, "malformedAddress"}, + {jss::authorize, "malformedAddress"}, + }); } } void testLedgerEntryDepositPreauth() { - testcase("ledger_entry Deposit Preauth"); + testcase("Deposit Preauth"); using namespace test::jtx; @@ -600,91 +875,21 @@ class LedgerEntry_test : public beast::unit_test::suite BEAST_EXPECT(jrr[jss::node][sfAuthorize.jsonName] == becky.human()); } { - // Malformed request: deposit_preauth neither object nor string. - Json::Value jvParams; - jvParams[jss::deposit_preauth] = -5; - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); - } - { - // Malformed request: deposit_preauth not hex string. - Json::Value jvParams; - jvParams[jss::deposit_preauth] = "0123456789ABCDEFG"; - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); - } - { - // Malformed request: missing [jss::deposit_preauth][jss::owner] - Json::Value jvParams; - jvParams[jss::deposit_preauth][jss::authorized] = becky.human(); - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); - } - { - // Malformed request: [jss::deposit_preauth][jss::owner] not string. - Json::Value jvParams; - jvParams[jss::deposit_preauth][jss::owner] = 7; - jvParams[jss::deposit_preauth][jss::authorized] = becky.human(); - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); - } - { - // Malformed: missing [jss::deposit_preauth][jss::authorized] - Json::Value jvParams; - jvParams[jss::deposit_preauth][jss::owner] = alice.human(); - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); - } - { - // Malformed: [jss::deposit_preauth][jss::authorized] not string. - Json::Value jvParams; - jvParams[jss::deposit_preauth][jss::owner] = alice.human(); - jvParams[jss::deposit_preauth][jss::authorized] = 47; - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); - } - { - // Malformed: [jss::deposit_preauth][jss::owner] is malformed. - Json::Value jvParams; - jvParams[jss::deposit_preauth][jss::owner] = - "rP6P9ypfAmc!pw8SZHNwM4nvZHFXDraQas"; - - jvParams[jss::deposit_preauth][jss::authorized] = becky.human(); - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedOwner", ""); - } - { - // Malformed: [jss::deposit_preauth][jss::authorized] is malformed. - Json::Value jvParams; - jvParams[jss::deposit_preauth][jss::owner] = alice.human(); - jvParams[jss::deposit_preauth][jss::authorized] = - "rP6P9ypfAmc!pw8SZHNwM4nvZHFXDraQas"; - - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedAuthorized", ""); + // test all missing/malformed field cases + runLedgerEntryTest( + env, + jss::deposit_preauth, + { + {jss::owner, "malformedOwner"}, + {jss::authorized, "malformedAuthorized", false}, + }); } } void testLedgerEntryDepositPreauthCred() { - testcase("ledger_entry Deposit Preauth with credentials"); + testcase("Deposit Preauth with credentials"); using namespace test::jtx; @@ -739,19 +944,30 @@ class LedgerEntry_test : public beast::unit_test::suite jvParams[jss::ledger_index] = jss::validated; jvParams[jss::deposit_preauth][jss::owner] = bob.human(); - jvParams[jss::deposit_preauth][jss::authorized_credentials] = - Json::arrayValue; - auto& arr( - jvParams[jss::deposit_preauth][jss::authorized_credentials]); + auto tryField = [&](Json::Value fieldValue) -> void { + Json::Value arr = Json::arrayValue; + Json::Value jo; + jo[jss::issuer] = fieldValue; + jo[jss::credential_type] = strHex(std::string_view(credType)); + arr.append(jo); + jvParams[jss::deposit_preauth][jss::authorized_credentials] = + arr; - Json::Value jo; - jo[jss::issuer] = to_string(xrpAccount()); - jo[jss::credential_type] = strHex(std::string_view(credType)); - arr.append(std::move(jo)); - auto const jrr = - env.rpc("json", "ledger_entry", to_string(jvParams)); - checkErrorValue( - jrr[jss::result], "malformedAuthorizedCredentials", ""); + Json::Value const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + auto const expectedErrMsg = fieldValue.isNull() + ? RPC::missing_field_message(jss::issuer.c_str()) + : RPC::expected_field_message(jss::issuer, "AccountID"); + checkErrorValue( + jrr, "malformedAuthorizedCredentials", expectedErrMsg); + }; + + auto const& badValues = getBadValues(FieldType::AccountField); + for (auto const& value : badValues) + { + tryField(value); + } + tryField(Json::nullValue); } { @@ -773,7 +989,10 @@ class LedgerEntry_test : public beast::unit_test::suite auto const jrr = env.rpc("json", "ledger_entry", to_string(jvParams)); checkErrorValue( - jrr[jss::result], "malformedAuthorizedCredentials", ""); + jrr[jss::result], + "malformedAuthorizedCredentials", + RPC::expected_field_message( + jss::authorized_credentials, "array")); } { @@ -782,20 +1001,31 @@ class LedgerEntry_test : public beast::unit_test::suite jvParams[jss::ledger_index] = jss::validated; jvParams[jss::deposit_preauth][jss::owner] = bob.human(); - jvParams[jss::deposit_preauth][jss::authorized_credentials] = - Json::arrayValue; - auto& arr( - jvParams[jss::deposit_preauth][jss::authorized_credentials]); + auto tryField = [&](Json::Value fieldValue) -> void { + Json::Value arr = Json::arrayValue; + Json::Value jo; + jo[jss::issuer] = issuer.human(); + jo[jss::credential_type] = fieldValue; + arr.append(jo); + jvParams[jss::deposit_preauth][jss::authorized_credentials] = + arr; - Json::Value jo; - jo[jss::issuer] = issuer.human(); - jo[jss::credential_type] = ""; - arr.append(std::move(jo)); + Json::Value const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + auto const expectedErrMsg = fieldValue.isNull() + ? RPC::missing_field_message(jss::credential_type.c_str()) + : RPC::expected_field_message( + jss::credential_type, "hex string"); + checkErrorValue( + jrr, "malformedAuthorizedCredentials", expectedErrMsg); + }; - auto const jrr = - env.rpc("json", "ledger_entry", to_string(jvParams)); - checkErrorValue( - jrr[jss::result], "malformedAuthorizedCredentials", ""); + auto const& badValues = getBadValues(FieldType::BlobField); + for (auto const& value : badValues) + { + tryField(value); + } + tryField(Json::nullValue); } { @@ -817,7 +1047,11 @@ class LedgerEntry_test : public beast::unit_test::suite auto const jrr = env.rpc("json", "ledger_entry", to_string(jvParams)); - checkErrorValue(jrr[jss::result], "malformedRequest", ""); + checkErrorValue( + jrr[jss::result], + "malformedRequest", + "Must have exactly one of `authorized` and " + "`authorized_credentials`."); } { @@ -825,11 +1059,14 @@ class LedgerEntry_test : public beast::unit_test::suite Json::Value jvParams; jvParams[jss::ledger_index] = jss::validated; jvParams[jss::deposit_preauth][jss::owner] = bob.human(); - jvParams[jss::deposit_preauth][jss::authorized_credentials] = 42; - - auto const jrr = - env.rpc("json", "ledger_entry", to_string(jvParams)); - checkErrorValue(jrr[jss::result], "malformedRequest", ""); + testMalformedSubfield( + env, + jvParams, + jss::deposit_preauth, + jss::authorized_credentials, + FieldType::ArrayField, + "malformedAuthorizedCredentials", + false); } { @@ -846,7 +1083,9 @@ class LedgerEntry_test : public beast::unit_test::suite auto const jrr = env.rpc("json", "ledger_entry", to_string(jvParams)); checkErrorValue( - jrr[jss::result], "malformedAuthorizedCredentials", ""); + jrr[jss::result], + "malformedAuthorizedCredentials", + "Invalid field 'authorized_credentials', not array."); } { @@ -865,7 +1104,9 @@ class LedgerEntry_test : public beast::unit_test::suite auto const jrr = env.rpc("json", "ledger_entry", to_string(jvParams)); checkErrorValue( - jrr[jss::result], "malformedAuthorizedCredentials", ""); + jrr[jss::result], + "malformedAuthorizedCredentials", + "Invalid field 'authorized_credentials', not array."); } { @@ -879,13 +1120,14 @@ class LedgerEntry_test : public beast::unit_test::suite auto const jrr = env.rpc("json", "ledger_entry", to_string(jvParams)); checkErrorValue( - jrr[jss::result], "malformedAuthorizedCredentials", ""); + jrr[jss::result], + "malformedAuthorizedCredentials", + "Invalid field 'authorized_credentials', not array."); } { // Failed, authorized_credentials is too long - - static std::string_view const credTypes[] = { + static std::array const credTypes = { "cred1", "cred2", "cred3", @@ -908,205 +1150,27 @@ class LedgerEntry_test : public beast::unit_test::suite auto& arr( jvParams[jss::deposit_preauth][jss::authorized_credentials]); - for (unsigned i = 0; i < sizeof(credTypes) / sizeof(credTypes[0]); - ++i) + for (auto cred : credTypes) { Json::Value jo; jo[jss::issuer] = issuer.human(); - jo[jss::credential_type] = - strHex(std::string_view(credTypes[i])); + jo[jss::credential_type] = strHex(std::string_view(cred)); arr.append(std::move(jo)); } auto const jrr = env.rpc("json", "ledger_entry", to_string(jvParams)); checkErrorValue( - jrr[jss::result], "malformedAuthorizedCredentials", ""); - } - - { - // Failed, issuer is not set - Json::Value jvParams; - jvParams[jss::ledger_index] = jss::validated; - jvParams[jss::deposit_preauth][jss::owner] = bob.human(); - - jvParams[jss::deposit_preauth][jss::authorized_credentials] = - Json::arrayValue; - auto& arr( - jvParams[jss::deposit_preauth][jss::authorized_credentials]); - - Json::Value jo; - jo[jss::credential_type] = strHex(std::string_view(credType)); - arr.append(std::move(jo)); - - auto const jrr = - env.rpc("json", "ledger_entry", to_string(jvParams)); - checkErrorValue( - jrr[jss::result], "malformedAuthorizedCredentials", ""); - } - - { - // Failed, issuer isn't string - Json::Value jvParams; - jvParams[jss::ledger_index] = jss::validated; - jvParams[jss::deposit_preauth][jss::owner] = bob.human(); - - jvParams[jss::deposit_preauth][jss::authorized_credentials] = - Json::arrayValue; - auto& arr( - jvParams[jss::deposit_preauth][jss::authorized_credentials]); - - Json::Value jo; - jo[jss::issuer] = 42; - jo[jss::credential_type] = strHex(std::string_view(credType)); - arr.append(std::move(jo)); - - auto const jrr = - env.rpc("json", "ledger_entry", to_string(jvParams)); - checkErrorValue( - jrr[jss::result], "malformedAuthorizedCredentials", ""); - } - - { - // Failed, issuer is an array - Json::Value jvParams; - jvParams[jss::ledger_index] = jss::validated; - jvParams[jss::deposit_preauth][jss::owner] = bob.human(); - - jvParams[jss::deposit_preauth][jss::authorized_credentials] = - Json::arrayValue; - auto& arr( - jvParams[jss::deposit_preauth][jss::authorized_credentials]); - - Json::Value jo; - Json::Value payload = Json::arrayValue; - payload.append(42); - jo[jss::issuer] = std::move(payload); - jo[jss::credential_type] = strHex(std::string_view(credType)); - arr.append(std::move(jo)); - - auto const jrr = - env.rpc("json", "ledger_entry", to_string(jvParams)); - checkErrorValue( - jrr[jss::result], "malformedAuthorizedCredentials", ""); - } - - { - // Failed, issuer isn't valid encoded account - Json::Value jvParams; - jvParams[jss::ledger_index] = jss::validated; - jvParams[jss::deposit_preauth][jss::owner] = bob.human(); - - jvParams[jss::deposit_preauth][jss::authorized_credentials] = - Json::arrayValue; - auto& arr( - jvParams[jss::deposit_preauth][jss::authorized_credentials]); - - Json::Value jo; - jo[jss::issuer] = "invalid_account"; - jo[jss::credential_type] = strHex(std::string_view(credType)); - arr.append(std::move(jo)); - - auto const jrr = - env.rpc("json", "ledger_entry", to_string(jvParams)); - checkErrorValue( - jrr[jss::result], "malformedAuthorizedCredentials", ""); - } - - { - // Failed, credential_type is not set - Json::Value jvParams; - jvParams[jss::ledger_index] = jss::validated; - jvParams[jss::deposit_preauth][jss::owner] = bob.human(); - - jvParams[jss::deposit_preauth][jss::authorized_credentials] = - Json::arrayValue; - auto& arr( - jvParams[jss::deposit_preauth][jss::authorized_credentials]); - - Json::Value jo; - jo[jss::issuer] = issuer.human(); - arr.append(std::move(jo)); - - auto const jrr = - env.rpc("json", "ledger_entry", to_string(jvParams)); - checkErrorValue( - jrr[jss::result], "malformedAuthorizedCredentials", ""); - } - - { - // Failed, credential_type isn't string - Json::Value jvParams; - jvParams[jss::ledger_index] = jss::validated; - jvParams[jss::deposit_preauth][jss::owner] = bob.human(); - - jvParams[jss::deposit_preauth][jss::authorized_credentials] = - Json::arrayValue; - auto& arr( - jvParams[jss::deposit_preauth][jss::authorized_credentials]); - - Json::Value jo; - jo[jss::issuer] = issuer.human(); - jo[jss::credential_type] = 42; - arr.append(std::move(jo)); - - auto const jrr = - env.rpc("json", "ledger_entry", to_string(jvParams)); - checkErrorValue( - jrr[jss::result], "malformedAuthorizedCredentials", ""); - } - - { - // Failed, credential_type is an array - Json::Value jvParams; - jvParams[jss::ledger_index] = jss::validated; - jvParams[jss::deposit_preauth][jss::owner] = bob.human(); - - jvParams[jss::deposit_preauth][jss::authorized_credentials] = - Json::arrayValue; - auto& arr( - jvParams[jss::deposit_preauth][jss::authorized_credentials]); - - Json::Value jo; - jo[jss::issuer] = issuer.human(); - Json::Value payload = Json::arrayValue; - payload.append(42); - jo[jss::credential_type] = std::move(payload); - arr.append(std::move(jo)); - - auto const jrr = - env.rpc("json", "ledger_entry", to_string(jvParams)); - checkErrorValue( - jrr[jss::result], "malformedAuthorizedCredentials", ""); - } - - { - // Failed, credential_type isn't hex encoded - Json::Value jvParams; - jvParams[jss::ledger_index] = jss::validated; - jvParams[jss::deposit_preauth][jss::owner] = bob.human(); - - jvParams[jss::deposit_preauth][jss::authorized_credentials] = - Json::arrayValue; - auto& arr( - jvParams[jss::deposit_preauth][jss::authorized_credentials]); - - Json::Value jo; - jo[jss::issuer] = issuer.human(); - jo[jss::credential_type] = "12KK"; - arr.append(std::move(jo)); - - auto const jrr = - env.rpc("json", "ledger_entry", to_string(jvParams)); - checkErrorValue( - jrr[jss::result], "malformedAuthorizedCredentials", ""); + jrr[jss::result], + "malformedAuthorizedCredentials", + "Invalid field 'authorized_credentials', not array."); } } void testLedgerEntryDirectory() { - testcase("ledger_entry Request Directory"); + testcase("Directory"); using namespace test::jtx; Env env{*this}; Account const alice{"alice"}; @@ -1188,39 +1252,48 @@ class LedgerEntry_test : public beast::unit_test::suite BEAST_EXPECT(jrr[jss::node][sfIndexes.jsonName].size() == 2); } { - // Null directory argument. + // Bad directory argument. Json::Value jvParams; - jvParams[jss::directory] = Json::nullValue; jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); + testMalformedField( + env, + jvParams, + jss::directory, + FieldType::HashOrObjectField, + "malformedRequest"); } { // Non-integer sub_index. Json::Value jvParams; jvParams[jss::directory] = Json::objectValue; jvParams[jss::directory][jss::dir_root] = dirRootIndex; - jvParams[jss::directory][jss::sub_index] = 1.5; jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); + testMalformedSubfield( + env, + jvParams, + jss::directory, + jss::sub_index, + FieldType::UInt64Field, + "malformedRequest", + false); } { // Malformed owner entry. Json::Value jvParams; jvParams[jss::directory] = Json::objectValue; - std::string const badAddress = makeBadAddress(alice.human()); - jvParams[jss::directory][jss::owner] = badAddress; jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedAddress", ""); + testMalformedSubfield( + env, + jvParams, + jss::directory, + jss::owner, + FieldType::AccountField, + "malformedAddress", + false); } { - // Malformed directory object. Specify both dir_root and owner. + // Malformed directory object. Specifies both dir_root and owner. Json::Value jvParams; jvParams[jss::directory] = Json::objectValue; jvParams[jss::directory][jss::owner] = alice.human(); @@ -1228,7 +1301,10 @@ class LedgerEntry_test : public beast::unit_test::suite jvParams[jss::ledger_hash] = ledgerHash; Json::Value const jrr = env.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); + checkErrorValue( + jrr, + "malformedRequest", + "Must have exactly one of `owner` and `dir_root` fields."); } { // Incomplete directory object. Missing both dir_root and owner. @@ -1238,14 +1314,17 @@ class LedgerEntry_test : public beast::unit_test::suite jvParams[jss::ledger_hash] = ledgerHash; Json::Value const jrr = env.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); + checkErrorValue( + jrr, + "malformedRequest", + "Must have exactly one of `owner` and `dir_root` fields."); } } void testLedgerEntryEscrow() { - testcase("ledger_entry Request Escrow"); + testcase("Escrow"); using namespace test::jtx; Env env{*this}; Account const alice{"alice"}; @@ -1296,56 +1375,18 @@ class LedgerEntry_test : public beast::unit_test::suite jrr[jss::node][jss::Amount] == XRP(333).value().getText()); } { - // Malformed owner entry. - Json::Value jvParams; - jvParams[jss::escrow] = Json::objectValue; - - std::string const badAddress = makeBadAddress(alice.human()); - jvParams[jss::escrow][jss::owner] = badAddress; - jvParams[jss::escrow][jss::seq] = env.seq(alice) - 1; - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedOwner", ""); - } - { - // Missing owner. - Json::Value jvParams; - jvParams[jss::escrow] = Json::objectValue; - jvParams[jss::escrow][jss::seq] = env.seq(alice) - 1; - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); - } - { - // Missing sequence. - Json::Value jvParams; - jvParams[jss::escrow] = Json::objectValue; - jvParams[jss::escrow][jss::owner] = alice.human(); - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); - } - { - // Non-integer sequence. - Json::Value jvParams; - jvParams[jss::escrow] = Json::objectValue; - jvParams[jss::escrow][jss::owner] = alice.human(); - jvParams[jss::escrow][jss::seq] = - std::to_string(env.seq(alice) - 1); - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); + // Malformed escrow fields + runLedgerEntryTest( + env, + jss::escrow, + {{jss::owner, "malformedOwner"}, {jss::seq, "malformedSeq"}}); } } void testLedgerEntryOffer() { - testcase("ledger_entry Request Offer"); + testcase("Offer"); using namespace test::jtx; Env env{*this}; Account const alice{"alice"}; @@ -1379,56 +1420,21 @@ class LedgerEntry_test : public beast::unit_test::suite "json", "ledger_entry", to_string(jvParams))[jss::result]; BEAST_EXPECT(jrr[jss::node][jss::TakerGets] == "322000000"); } - { - // Malformed account entry. - Json::Value jvParams; - jvParams[jss::offer] = Json::objectValue; - std::string const badAddress = makeBadAddress(alice.human()); - jvParams[jss::offer][jss::account] = badAddress; - jvParams[jss::offer][jss::seq] = env.seq(alice) - 1; - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedAddress", ""); - } { - // Malformed offer object. Missing account member. - Json::Value jvParams; - jvParams[jss::offer] = Json::objectValue; - jvParams[jss::offer][jss::seq] = env.seq(alice) - 1; - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); - } - { - // Malformed offer object. Missing seq member. - Json::Value jvParams; - jvParams[jss::offer] = Json::objectValue; - jvParams[jss::offer][jss::account] = alice.human(); - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); - } - { - // Malformed offer object. Non-integral seq member. - Json::Value jvParams; - jvParams[jss::offer] = Json::objectValue; - jvParams[jss::offer][jss::account] = alice.human(); - jvParams[jss::offer][jss::seq] = std::to_string(env.seq(alice) - 1); - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); + // Malformed offer fields + runLedgerEntryTest( + env, + jss::offer, + {{jss::account, "malformedAddress"}, + {jss::seq, "malformedRequest"}}); } } void testLedgerEntryPayChan() { - testcase("ledger_entry Request Pay Chan"); + testcase("Pay Chan"); using namespace test::jtx; using namespace std::literals::chrono_literals; Env env{*this}; @@ -1478,14 +1484,19 @@ class LedgerEntry_test : public beast::unit_test::suite jvParams[jss::ledger_hash] = ledgerHash; Json::Value const jrr = env.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "entryNotFound", ""); + checkErrorValue(jrr, "entryNotFound", "Entry not found."); + } + + { + // Malformed paychan field + runLedgerEntryTest(env, jss::payment_channel); } } void testLedgerEntryRippleState() { - testcase("ledger_entry Request RippleState"); + testcase("RippleState"); using namespace test::jtx; Env env{*this}; Account const alice{"alice"}; @@ -1521,36 +1532,14 @@ class LedgerEntry_test : public beast::unit_test::suite jrr[jss::node][sfHighLimit.jsonName][jss::value] == "999"); } { - // ripple_state is not an object. - Json::Value jvParams; - jvParams[fieldName] = "ripple_state"; - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); - } - { - // ripple_state.currency is missing. - Json::Value jvParams; - jvParams[fieldName] = Json::objectValue; - jvParams[fieldName][jss::accounts] = Json::arrayValue; - jvParams[fieldName][jss::accounts][0u] = alice.human(); - jvParams[fieldName][jss::accounts][1u] = gw.human(); - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); - } - { - // ripple_state accounts is not an array. - Json::Value jvParams; - jvParams[fieldName] = Json::objectValue; - jvParams[fieldName][jss::accounts] = 2; - jvParams[fieldName][jss::currency] = "USD"; - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); + // test basic malformed scenarios + runLedgerEntryTest( + env, + fieldName, + { + {jss::accounts, "malformedRequest"}, + {jss::currency, "malformedCurrency"}, + }); } { // ripple_state one of the accounts is missing. @@ -1562,7 +1551,11 @@ class LedgerEntry_test : public beast::unit_test::suite jvParams[jss::ledger_hash] = ledgerHash; Json::Value const jrr = env.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); + checkErrorValue( + jrr, + "malformedRequest", + "Invalid field 'accounts', not length-2 array of " + "Accounts."); } { // ripple_state more than 2 accounts. @@ -1576,33 +1569,60 @@ class LedgerEntry_test : public beast::unit_test::suite jvParams[jss::ledger_hash] = ledgerHash; Json::Value const jrr = env.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); + checkErrorValue( + jrr, + "malformedRequest", + "Invalid field 'accounts', not length-2 array of " + "Accounts."); } { - // ripple_state account[0] is not a string. + // ripple_state account[0] / account[1] is not an account. Json::Value jvParams; jvParams[fieldName] = Json::objectValue; - jvParams[fieldName][jss::accounts] = Json::arrayValue; - jvParams[fieldName][jss::accounts][0u] = 44; - jvParams[fieldName][jss::accounts][1u] = gw.human(); - jvParams[fieldName][jss::currency] = "USD"; - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); - } - { - // ripple_state account[1] is not a string. - Json::Value jvParams; - jvParams[fieldName] = Json::objectValue; - jvParams[fieldName][jss::accounts] = Json::arrayValue; - jvParams[fieldName][jss::accounts][0u] = alice.human(); - jvParams[fieldName][jss::accounts][1u] = 21; - jvParams[fieldName][jss::currency] = "USD"; - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); + auto tryField = [&](Json::Value badAccount) -> void { + { + // account[0] + jvParams[fieldName][jss::accounts] = Json::arrayValue; + jvParams[fieldName][jss::accounts][0u] = badAccount; + jvParams[fieldName][jss::accounts][1u] = gw.human(); + jvParams[fieldName][jss::currency] = "USD"; + + Json::Value const jrr = env.rpc( + "json", + "ledger_entry", + to_string(jvParams))[jss::result]; + checkErrorValue( + jrr, + "malformedAddress", + RPC::expected_field_message( + jss::accounts, "array of Accounts")); + } + + { + // account[1] + jvParams[fieldName][jss::accounts] = Json::arrayValue; + jvParams[fieldName][jss::accounts][0u] = alice.human(); + jvParams[fieldName][jss::accounts][1u] = badAccount; + jvParams[fieldName][jss::currency] = "USD"; + + Json::Value const jrr = env.rpc( + "json", + "ledger_entry", + to_string(jvParams))[jss::result]; + checkErrorValue( + jrr, + "malformedAddress", + RPC::expected_field_message( + jss::accounts, "array of Accounts")); + } + }; + + auto const& badValues = getBadValues(FieldType::AccountField); + for (auto const& value : badValues) + { + tryField(value); + } + tryField(Json::nullValue); } { // ripple_state account[0] == account[1]. @@ -1615,48 +1635,10 @@ class LedgerEntry_test : public beast::unit_test::suite jvParams[jss::ledger_hash] = ledgerHash; Json::Value const jrr = env.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); - } - { - // ripple_state malformed account[0]. - Json::Value jvParams; - jvParams[fieldName] = Json::objectValue; - jvParams[fieldName][jss::accounts] = Json::arrayValue; - jvParams[fieldName][jss::accounts][0u] = - makeBadAddress(alice.human()); - jvParams[fieldName][jss::accounts][1u] = gw.human(); - jvParams[fieldName][jss::currency] = "USD"; - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedAddress", ""); - } - { - // ripple_state malformed account[1]. - Json::Value jvParams; - jvParams[fieldName] = Json::objectValue; - jvParams[fieldName][jss::accounts] = Json::arrayValue; - jvParams[fieldName][jss::accounts][0u] = alice.human(); - jvParams[fieldName][jss::accounts][1u] = - makeBadAddress(gw.human()); - jvParams[fieldName][jss::currency] = "USD"; - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedAddress", ""); - } - { - // ripple_state malformed currency. - Json::Value jvParams; - jvParams[fieldName] = Json::objectValue; - jvParams[fieldName][jss::accounts] = Json::arrayValue; - jvParams[fieldName][jss::accounts][0u] = alice.human(); - jvParams[fieldName][jss::accounts][1u] = gw.human(); - jvParams[fieldName][jss::currency] = "USDollars"; - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedCurrency", ""); + checkErrorValue( + jrr, + "malformedRequest", + "Cannot have a trustline to self."); } } } @@ -1664,7 +1646,7 @@ class LedgerEntry_test : public beast::unit_test::suite void testLedgerEntryTicket() { - testcase("ledger_entry Request Ticket"); + testcase("Ticket"); using namespace test::jtx; Env env{*this}; env.close(); @@ -1686,7 +1668,7 @@ class LedgerEntry_test : public beast::unit_test::suite jvParams[jss::ledger_hash] = ledgerHash; Json::Value const jrr = env.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "entryNotFound", ""); + checkErrorValue(jrr, "entryNotFound", "Entry not found."); } { // First real ticket requested by index. @@ -1721,7 +1703,7 @@ class LedgerEntry_test : public beast::unit_test::suite jvParams[jss::ledger_hash] = ledgerHash; Json::Value const jrr = env.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "entryNotFound", ""); + checkErrorValue(jrr, "entryNotFound", "Entry not found."); } { // Request a ticket using an account root entry. @@ -1730,59 +1712,26 @@ class LedgerEntry_test : public beast::unit_test::suite jvParams[jss::ledger_hash] = ledgerHash; Json::Value const jrr = env.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "unexpectedLedgerType", ""); + checkErrorValue( + jrr, "unexpectedLedgerType", "Unexpected ledger type."); } - { - // Malformed account entry. - Json::Value jvParams; - jvParams[jss::ticket] = Json::objectValue; - std::string const badAddress = makeBadAddress(env.master.human()); - jvParams[jss::ticket][jss::account] = badAddress; - jvParams[jss::ticket][jss::ticket_seq] = env.seq(env.master) - 1; - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedAddress", ""); - } { - // Malformed ticket object. Missing account member. - Json::Value jvParams; - jvParams[jss::ticket] = Json::objectValue; - jvParams[jss::ticket][jss::ticket_seq] = env.seq(env.master) - 1; - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); - } - { - // Malformed ticket object. Missing seq member. - Json::Value jvParams; - jvParams[jss::ticket] = Json::objectValue; - jvParams[jss::ticket][jss::account] = env.master.human(); - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); - } - { - // Malformed ticket object. Non-integral seq member. - Json::Value jvParams; - jvParams[jss::ticket] = Json::objectValue; - jvParams[jss::ticket][jss::account] = env.master.human(); - jvParams[jss::ticket][jss::ticket_seq] = - std::to_string(env.seq(env.master) - 1); - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); + // test basic malformed scenarios + runLedgerEntryTest( + env, + jss::ticket, + { + {jss::account, "malformedAddress"}, + {jss::ticket_seq, "malformedRequest"}, + }); } } void testLedgerEntryDID() { - testcase("ledger_entry Request DID"); + testcase("DID"); using namespace test::jtx; using namespace std::literals::chrono_literals; Env env{*this}; @@ -1826,230 +1775,17 @@ class LedgerEntry_test : public beast::unit_test::suite jvParams[jss::ledger_hash] = ledgerHash; Json::Value const jrr = env.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "entryNotFound", ""); + checkErrorValue(jrr, "entryNotFound", "Entry not found."); } - } - - void - testLedgerEntryInvalidParams(unsigned int apiVersion) - { - testcase( - "ledger_entry Request With Invalid Parameters v" + - std::to_string(apiVersion)); - using namespace test::jtx; - Env env{*this}; - - std::string const ledgerHash{to_string(env.closed()->info().hash)}; - - auto makeParams = [&apiVersion](std::function f) { - Json::Value params; - params[jss::api_version] = apiVersion; - f(params); - return params; - }; - // "features" is not an option supported by ledger_entry. { - auto const jvParams = - makeParams([&ledgerHash](Json::Value& jvParams) { - jvParams[jss::features] = ledgerHash; - jvParams[jss::ledger_hash] = ledgerHash; - }); - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - - if (apiVersion < 2u) - checkErrorValue(jrr, "unknownOption", ""); - else - checkErrorValue(jrr, "invalidParams", ""); - } - Json::Value const injectObject = []() { - Json::Value obj(Json::objectValue); - obj[jss::account] = "rhigTLJJyXXSRUyRCQtqi1NoAZZzZnS4KU"; - obj[jss::ledger_index] = "validated"; - return obj; - }(); - Json::Value const injectArray = []() { - Json::Value arr(Json::arrayValue); - arr[0u] = "rhigTLJJyXXSRUyRCQtqi1NoAZZzZnS4KU"; - arr[1u] = "validated"; - return arr; - }(); - - // invalid input for fields that can handle an object, but can't handle - // an array - for (auto const& field : - {jss::directory, jss::escrow, jss::offer, jss::ticket, jss::amm}) - { - auto const jvParams = - makeParams([&field, &injectArray](Json::Value& jvParams) { - jvParams[field] = injectArray; - }); - - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - - if (apiVersion < 2u) - checkErrorValue(jrr, "internal", "Internal error."); - else - checkErrorValue(jrr, "invalidParams", ""); - } - // Fields that can handle objects just fine - for (auto const& field : - {jss::directory, jss::escrow, jss::offer, jss::ticket, jss::amm}) - { - auto const jvParams = - makeParams([&field, &injectObject](Json::Value& jvParams) { - jvParams[field] = injectObject; - }); - - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - - checkErrorValue(jrr, "malformedRequest", ""); - } - - for (auto const& inject : {injectObject, injectArray}) - { - // invalid input for fields that can't handle an object or an array - for (auto const& field : - {jss::index, - jss::account_root, - jss::check, - jss::payment_channel}) - { - auto const jvParams = - makeParams([&field, &inject](Json::Value& jvParams) { - jvParams[field] = inject; - }); - - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - - if (apiVersion < 2u) - checkErrorValue(jrr, "internal", "Internal error."); - else - checkErrorValue(jrr, "invalidParams", ""); - } - // directory sub-fields - for (auto const& field : {jss::dir_root, jss::owner}) - { - auto const jvParams = - makeParams([&field, &inject](Json::Value& jvParams) { - jvParams[jss::directory][field] = inject; - }); - - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - - if (apiVersion < 2u) - checkErrorValue(jrr, "internal", "Internal error."); - else - checkErrorValue(jrr, "invalidParams", ""); - } - // escrow sub-fields - { - auto const jvParams = - makeParams([&inject](Json::Value& jvParams) { - jvParams[jss::escrow][jss::owner] = inject; - jvParams[jss::escrow][jss::seq] = 99; - }); - - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - - if (apiVersion < 2u) - checkErrorValue(jrr, "internal", "Internal error."); - else - checkErrorValue(jrr, "invalidParams", ""); - } - // offer sub-fields - { - auto const jvParams = - makeParams([&inject](Json::Value& jvParams) { - jvParams[jss::offer][jss::account] = inject; - jvParams[jss::offer][jss::seq] = 99; - }); - - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - - if (apiVersion < 2u) - checkErrorValue(jrr, "internal", "Internal error."); - else - checkErrorValue(jrr, "invalidParams", ""); - } - // ripple_state sub-fields - { - auto const jvParams = - makeParams([&inject](Json::Value& jvParams) { - Json::Value rs(Json::objectValue); - rs[jss::currency] = "FOO"; - rs[jss::accounts] = Json::Value(Json::arrayValue); - rs[jss::accounts][0u] = - "rhigTLJJyXXSRUyRCQtqi1NoAZZzZnS4KU"; - rs[jss::accounts][1u] = - "rKssEq6pg1KbqEqAFnua5mFAL6Ggpsh2wv"; - rs[jss::currency] = inject; - jvParams[jss::ripple_state] = std::move(rs); - }); - - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - - if (apiVersion < 2u) - checkErrorValue(jrr, "internal", "Internal error."); - else - checkErrorValue(jrr, "invalidParams", ""); - } - // ticket sub-fields - { - auto const jvParams = - makeParams([&inject](Json::Value& jvParams) { - jvParams[jss::ticket][jss::account] = inject; - jvParams[jss::ticket][jss::ticket_seq] = 99; - }); - - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - - if (apiVersion < 2u) - checkErrorValue(jrr, "internal", "Internal error."); - else - checkErrorValue(jrr, "invalidParams", ""); - } - - // Fields that can handle malformed inputs just fine - for (auto const& field : {jss::nft_page, jss::deposit_preauth}) - { - auto const jvParams = - makeParams([&field, &inject](Json::Value& jvParams) { - jvParams[field] = inject; - }); - - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - - checkErrorValue(jrr, "malformedRequest", ""); - } - // Subfields of deposit_preauth that can handle malformed inputs - // fine - for (auto const& field : {jss::owner, jss::authorized}) - { - auto const jvParams = - makeParams([&field, &inject](Json::Value& jvParams) { - auto pa = Json::Value(Json::objectValue); - pa[jss::owner] = "rhigTLJJyXXSRUyRCQtqi1NoAZZzZnS4KU"; - pa[jss::authorized] = - "rKssEq6pg1KbqEqAFnua5mFAL6Ggpsh2wv"; - pa[field] = inject; - jvParams[jss::deposit_preauth] = std::move(pa); - }); - - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - - checkErrorValue(jrr, "malformedRequest", ""); - } + // Malformed DID index + Json::Value jvParams; + testMalformedField( + env, + jvParams, + jss::did, + FieldType::AccountField, + "malformedAddress"); } } @@ -2068,28 +1804,16 @@ class LedgerEntry_test : public beast::unit_test::suite {.owner = owner, .fee = static_cast(env.current()->fees().base.drops())}); - // Malformed document id - auto res = Oracle::ledgerEntry(env, owner, NoneTag); - BEAST_EXPECT(res[jss::error].asString() == "invalidParams"); - std::vector invalid = {-1, 1.2, "", "Invalid"}; - for (auto const& v : invalid) { - auto const res = Oracle::ledgerEntry(env, owner, v); - BEAST_EXPECT(res[jss::error].asString() == "malformedDocumentID"); + // test basic malformed scenarios + runLedgerEntryTest( + env, + jss::oracle, + { + {jss::account, "malformedAccount"}, + {jss::oracle_document_id, "malformedDocumentID"}, + }); } - // Missing document id - res = Oracle::ledgerEntry(env, owner, std::nullopt); - BEAST_EXPECT(res[jss::error].asString() == "malformedRequest"); - - // Missing account - res = Oracle::ledgerEntry(env, std::nullopt, 1); - BEAST_EXPECT(res[jss::error].asString() == "malformedRequest"); - - // Malformed account - std::string malfAccount = to_string(owner.id()); - malfAccount.replace(10, 1, 1, '!'); - res = Oracle::ledgerEntry(env, malfAccount, 1); - BEAST_EXPECT(res[jss::error].asString() == "malformedAddress"); } void @@ -2144,7 +1868,7 @@ class LedgerEntry_test : public beast::unit_test::suite void testLedgerEntryMPT() { - testcase("ledger_entry Request MPT"); + testcase("MPT"); using namespace test::jtx; using namespace std::literals::chrono_literals; Env env{*this}; @@ -2185,7 +1909,7 @@ class LedgerEntry_test : public beast::unit_test::suite jvParams[jss::ledger_hash] = ledgerHash; Json::Value const jrr = env.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "entryNotFound", ""); + checkErrorValue(jrr, "entryNotFound", "Entry not found."); } { // Request the MPToken using its owner + mptIssuanceID. @@ -2210,14 +1934,24 @@ class LedgerEntry_test : public beast::unit_test::suite jvParams[jss::ledger_hash] = ledgerHash; Json::Value const jrr = env.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "entryNotFound", ""); + checkErrorValue(jrr, "entryNotFound", "Entry not found."); + } + { + // Malformed MPTIssuance index + Json::Value jvParams; + testMalformedField( + env, + jvParams, + jss::mptoken, + FieldType::HashOrObjectField, + "malformedRequest"); } } void testLedgerEntryPermissionedDomain() { - testcase("ledger_entry PermissionedDomain"); + testcase("PermissionedDomain"); using namespace test::jtx; @@ -2278,73 +2012,25 @@ class LedgerEntry_test : public beast::unit_test::suite "12F1F1F1F180D67377B2FAB292A31C922470326268D2B9B74CD1E582645B9A" "DE"; auto const jrr = env.rpc("json", "ledger_entry", to_string(params)); - checkErrorValue(jrr[jss::result], "entryNotFound", ""); + checkErrorValue( + jrr[jss::result], "entryNotFound", "Entry not found."); } - { - // Fail, invalid permissioned domain index - Json::Value params; - params[jss::ledger_index] = jss::validated; - params[jss::permissioned_domain] = "NotAHexString"; - auto const jrr = env.rpc("json", "ledger_entry", to_string(params)); - checkErrorValue(jrr[jss::result], "malformedRequest", ""); - } - - { - // Fail, permissioned domain is not an object - Json::Value params; - params[jss::ledger_index] = jss::validated; - params[jss::permissioned_domain] = 10; - auto const jrr = env.rpc("json", "ledger_entry", to_string(params)); - checkErrorValue(jrr[jss::result], "malformedRequest", ""); - } - - { - // Fail, invalid account - Json::Value params; - params[jss::ledger_index] = jss::validated; - params[jss::permissioned_domain][jss::account] = 1; - params[jss::permissioned_domain][jss::seq] = seq; - auto const jrr = env.rpc("json", "ledger_entry", to_string(params)); - checkErrorValue(jrr[jss::result], "malformedAddress", ""); - } - - { - // Fail, account is an object - Json::Value params; - params[jss::ledger_index] = jss::validated; - params[jss::permissioned_domain][jss::account] = - Json::Value{Json::ValueType::objectValue}; - params[jss::permissioned_domain][jss::seq] = seq; - auto const jrr = env.rpc("json", "ledger_entry", to_string(params)); - checkErrorValue(jrr[jss::result], "malformedAddress", ""); - } - - { - // Fail, no account - Json::Value params; - params[jss::ledger_index] = jss::validated; - params[jss::permissioned_domain][jss::account] = ""; - params[jss::permissioned_domain][jss::seq] = seq; - auto const jrr = env.rpc("json", "ledger_entry", to_string(params)); - checkErrorValue(jrr[jss::result], "malformedAddress", ""); - } - - { - // Fail, invalid sequence - Json::Value params; - params[jss::ledger_index] = jss::validated; - params[jss::permissioned_domain][jss::account] = alice.human(); - params[jss::permissioned_domain][jss::seq] = "12g"; - auto const jrr = env.rpc("json", "ledger_entry", to_string(params)); - checkErrorValue(jrr[jss::result], "malformedRequest", ""); + // test basic malformed scenarios + runLedgerEntryTest( + env, + jss::permissioned_domain, + { + {jss::account, "malformedAddress"}, + {jss::seq, "malformedRequest"}, + }); } } void testLedgerEntryCLI() { - testcase("ledger_entry command-line"); + testcase("command-line"); using namespace test::jtx; Env env{*this}; @@ -2391,9 +2077,6 @@ public: testLedgerEntryMPT(); testLedgerEntryPermissionedDomain(); testLedgerEntryCLI(); - - forAllApiVersions(std::bind_front( - &LedgerEntry_test::testLedgerEntryInvalidParams, this)); } }; @@ -2444,7 +2127,6 @@ class LedgerEntry_XChain_test : public beast::unit_test::suite, BEAST_EXPECT(jrr.isMember(jss::node)); auto r = jrr[jss::node]; - // std::cout << to_string(r) << '\n'; BEAST_EXPECT(r.isMember(jss::Account)); BEAST_EXPECT(r[jss::Account] == mcDoor.human()); @@ -2486,7 +2168,7 @@ class LedgerEntry_XChain_test : public beast::unit_test::suite, Json::Value const jrr = mcEnv.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "entryNotFound", ""); + checkErrorValue(jrr, "entryNotFound", "Entry not found."); } { // create two claim ids and verify that the bridge counter was @@ -2500,7 +2182,6 @@ class LedgerEntry_XChain_test : public beast::unit_test::suite, Json::Value jvParams; jvParams[jss::bridge_account] = mcDoor.human(); jvParams[jss::bridge] = jvb; - // std::cout << to_string(jvParams) << '\n'; Json::Value const jrr = mcEnv.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; @@ -2536,13 +2217,11 @@ class LedgerEntry_XChain_test : public beast::unit_test::suite, jvParams[jss::xchain_owned_claim_id] = jvXRPBridgeRPC; jvParams[jss::xchain_owned_claim_id][jss::xchain_owned_claim_id] = 1; - // std::cout << to_string(jvParams) << '\n'; Json::Value const jrr = scEnv.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; BEAST_EXPECT(jrr.isMember(jss::node)); auto r = jrr[jss::node]; - // std::cout << to_string(r) << '\n'; BEAST_EXPECT(r.isMember(jss::Account)); BEAST_EXPECT(r[jss::Account] == scAlice.human()); @@ -2563,7 +2242,6 @@ class LedgerEntry_XChain_test : public beast::unit_test::suite, BEAST_EXPECT(jrr.isMember(jss::node)); auto r = jrr[jss::node]; - // std::cout << to_string(r) << '\n'; BEAST_EXPECT(r.isMember(jss::Account)); BEAST_EXPECT(r[jss::Account] == scBob.human()); @@ -2622,10 +2300,8 @@ class LedgerEntry_XChain_test : public beast::unit_test::suite, jvXRPBridgeRPC; jvParams[jss::xchain_owned_create_account_claim_id] [jss::xchain_owned_create_account_claim_id] = 1; - // std::cout << to_string(jvParams) << '\n'; Json::Value const jrr = scEnv.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; - // std::cout << to_string(jrr) << '\n'; BEAST_EXPECT(jrr.isMember(jss::node)); auto r = jrr[jss::node]; @@ -2694,10 +2370,9 @@ class LedgerEntry_XChain_test : public beast::unit_test::suite, jvXRPBridgeRPC; jvParams[jss::xchain_owned_create_account_claim_id] [jss::xchain_owned_create_account_claim_id] = 1; - // std::cout << to_string(jvParams) << '\n'; Json::Value const jrr = scEnv.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "entryNotFound", ""); + checkErrorValue(jrr, "entryNotFound", "Entry not found."); } } diff --git a/src/xrpld/overlay/detail/OverlayImpl.cpp b/src/xrpld/overlay/detail/OverlayImpl.cpp index f2c683b69f..8d295faace 100644 --- a/src/xrpld/overlay/detail/OverlayImpl.cpp +++ b/src/xrpld/overlay/detail/OverlayImpl.cpp @@ -195,14 +195,16 @@ OverlayImpl::onHandoff( if (consumer.disconnect(journal)) return handoff; - auto const slot = m_peerFinder->new_inbound_slot( + auto const [slot, result] = m_peerFinder->new_inbound_slot( beast::IPAddressConversion::from_asio(local_endpoint), beast::IPAddressConversion::from_asio(remote_endpoint)); if (slot == nullptr) { - // self-connect, close + // connection refused either IP limit exceeded or self-connect handoff.moved = false; + JLOG(journal.debug()) + << "Peer " << remote_endpoint << " refused, " << to_string(result); return handoff; } @@ -402,10 +404,11 @@ OverlayImpl::connect(beast::IP::Endpoint const& remote_endpoint) return; } - auto const slot = peerFinder().new_outbound_slot(remote_endpoint); + auto const [slot, result] = peerFinder().new_outbound_slot(remote_endpoint); if (slot == nullptr) { - JLOG(journal_.debug()) << "Connect: No slot for " << remote_endpoint; + JLOG(journal_.debug()) << "Connect: No slot for " << remote_endpoint + << ": " << to_string(result); return; } diff --git a/src/xrpld/peerfinder/PeerfinderManager.h b/src/xrpld/peerfinder/PeerfinderManager.h index f399251c38..b7ea738a81 100644 --- a/src/xrpld/peerfinder/PeerfinderManager.h +++ b/src/xrpld/peerfinder/PeerfinderManager.h @@ -109,6 +109,9 @@ struct Config std::uint16_t port, bool validationPublicKey, int ipLimit); + + friend bool + operator==(Config const& lhs, Config const& rhs); }; //------------------------------------------------------------------------------ @@ -136,7 +139,13 @@ using Endpoints = std::vector; //------------------------------------------------------------------------------ /** Possible results from activating a slot. */ -enum class Result { duplicate, full, success }; +enum class Result { + inboundDisabled, + duplicatePeer, + ipLimitExceeded, + full, + success +}; /** * @brief Converts a `Result` enum value to its string representation. @@ -157,12 +166,16 @@ to_string(Result result) noexcept { switch (result) { - case Result::success: - return "success"; - case Result::duplicate: - return "duplicate connection"; + case Result::inboundDisabled: + return "inbound disabled"; + case Result::duplicatePeer: + return "peer already connected"; + case Result::ipLimitExceeded: + return "ip limit exceeded"; case Result::full: return "slots full"; + case Result::success: + return "success"; } return "unknown"; @@ -234,7 +247,7 @@ public: If nullptr is returned, then the slot could not be assigned. Usually this is because of a detected self-connection. */ - virtual std::shared_ptr + virtual std::pair, Result> new_inbound_slot( beast::IP::Endpoint const& local_endpoint, beast::IP::Endpoint const& remote_endpoint) = 0; @@ -243,7 +256,7 @@ public: If nullptr is returned, then the slot could not be assigned. Usually this is because of a duplicate connection. */ - virtual std::shared_ptr + virtual std::pair, Result> new_outbound_slot(beast::IP::Endpoint const& remote_endpoint) = 0; /** Called when mtENDPOINTS is received. */ diff --git a/src/xrpld/peerfinder/detail/Counts.h b/src/xrpld/peerfinder/detail/Counts.h index c91b27b026..821431c5bb 100644 --- a/src/xrpld/peerfinder/detail/Counts.h +++ b/src/xrpld/peerfinder/detail/Counts.h @@ -163,7 +163,7 @@ public: /** Returns the total number of inbound slots. */ int - inboundSlots() const + in_max() const { return m_in_max; } diff --git a/src/xrpld/peerfinder/detail/Logic.h b/src/xrpld/peerfinder/detail/Logic.h index b3922f63b3..9da2e55726 100644 --- a/src/xrpld/peerfinder/detail/Logic.h +++ b/src/xrpld/peerfinder/detail/Logic.h @@ -172,9 +172,7 @@ public: void addFixedPeer(std::string const& name, beast::IP::Endpoint const& ep) { - std::vector v; - v.push_back(ep); - addFixedPeer(name, v); + addFixedPeer(name, std::vector{ep}); } void @@ -261,7 +259,7 @@ public: //-------------------------------------------------------------------------- - SlotImp::ptr + std::pair new_inbound_slot( beast::IP::Endpoint const& local_endpoint, beast::IP::Endpoint const& remote_endpoint) @@ -277,12 +275,12 @@ public: { auto const count = connectedAddresses_.count(remote_endpoint.address()); - if (count > config_.ipLimit) + if (count + 1 > config_.ipLimit) { JLOG(m_journal.debug()) << beast::leftw(18) << "Logic dropping inbound " << remote_endpoint << " because of ip limits."; - return SlotImp::ptr(); + return {SlotImp::ptr(), Result::ipLimitExceeded}; } } @@ -292,7 +290,7 @@ public: JLOG(m_journal.debug()) << beast::leftw(18) << "Logic dropping " << remote_endpoint << " as duplicate incoming"; - return SlotImp::ptr(); + return {SlotImp::ptr(), Result::duplicatePeer}; } // Create the slot @@ -314,11 +312,11 @@ public: // Update counts counts_.add(*slot); - return result.first->second; + return {result.first->second, Result::success}; } // Can't check for self-connect because we don't know the local endpoint - SlotImp::ptr + std::pair new_outbound_slot(beast::IP::Endpoint const& remote_endpoint) { JLOG(m_journal.debug()) @@ -332,7 +330,7 @@ public: JLOG(m_journal.debug()) << beast::leftw(18) << "Logic dropping " << remote_endpoint << " as duplicate connect"; - return SlotImp::ptr(); + return {SlotImp::ptr(), Result::duplicatePeer}; } // Create the slot @@ -353,7 +351,7 @@ public: // Update counts counts_.add(*slot); - return result.first->second; + return {result.first->second, Result::success}; } bool @@ -417,7 +415,7 @@ public: // Check for duplicate connection by key if (keys_.find(key) != keys_.end()) - return Result::duplicate; + return Result::duplicatePeer; // If the peer belongs to a cluster or is reserved, // update the slot to reflect that. @@ -430,6 +428,8 @@ public: { if (!slot->inbound()) bootcache_.on_success(slot->remote_endpoint()); + if (slot->inbound() && counts_.in_max() == 0) + return Result::inboundDisabled; return Result::full; } @@ -651,7 +651,7 @@ public: // 2. We have slots // 3. We haven't failed the firewalled test // - if (config_.wantIncoming && counts_.inboundSlots() > 0) + if (config_.wantIncoming && counts_.in_max() > 0) { Endpoint ep; ep.hops = 0; diff --git a/src/xrpld/peerfinder/detail/PeerfinderConfig.cpp b/src/xrpld/peerfinder/detail/PeerfinderConfig.cpp index 3075224189..30eb778770 100644 --- a/src/xrpld/peerfinder/detail/PeerfinderConfig.cpp +++ b/src/xrpld/peerfinder/detail/PeerfinderConfig.cpp @@ -34,6 +34,17 @@ Config::Config() { } +bool +operator==(Config const& lhs, Config const& rhs) +{ + return lhs.autoConnect == rhs.autoConnect && + lhs.peerPrivate == rhs.peerPrivate && + lhs.wantIncoming == rhs.wantIncoming && lhs.inPeers == rhs.inPeers && + lhs.maxPeers == rhs.maxPeers && lhs.outPeers == rhs.outPeers && + lhs.features == lhs.features && lhs.ipLimit == rhs.ipLimit && + lhs.listeningPort == rhs.listeningPort; +} + std::size_t Config::calcOutPeers() const { diff --git a/src/xrpld/peerfinder/detail/PeerfinderManager.cpp b/src/xrpld/peerfinder/detail/PeerfinderManager.cpp index 205df67fa6..462820cca2 100644 --- a/src/xrpld/peerfinder/detail/PeerfinderManager.cpp +++ b/src/xrpld/peerfinder/detail/PeerfinderManager.cpp @@ -125,7 +125,7 @@ public: //-------------------------------------------------------------------------- - std::shared_ptr + std::pair, Result> new_inbound_slot( beast::IP::Endpoint const& local_endpoint, beast::IP::Endpoint const& remote_endpoint) override @@ -133,7 +133,7 @@ public: return m_logic.new_inbound_slot(local_endpoint, remote_endpoint); } - std::shared_ptr + std::pair, Result> new_outbound_slot(beast::IP::Endpoint const& remote_endpoint) override { return m_logic.new_outbound_slot(remote_endpoint); diff --git a/src/xrpld/rpc/detail/RPCHelpers.cpp b/src/xrpld/rpc/detail/RPCHelpers.cpp index b98f31340a..52a69eb79e 100644 --- a/src/xrpld/rpc/detail/RPCHelpers.cpp +++ b/src/xrpld/rpc/detail/RPCHelpers.cpp @@ -190,7 +190,7 @@ getAccountObjects( auto& jvObjects = (jvResult[jss::account_objects] = Json::arrayValue); - // this is a mutable version of limit, used to seemlessly switch + // this is a mutable version of limit, used to seamlessly switch // to iterating directory entries when nftokenpages are exhausted uint32_t mlimit = limit; @@ -373,7 +373,7 @@ ledgerFromRequest(T& ledger, JsonContext& context) indexValue = legacyLedger; } - if (hashValue) + if (!hashValue.isNull()) { if (!hashValue.isString()) return {rpcINVALID_PARAMS, "ledgerHashNotString"}; @@ -384,6 +384,9 @@ ledgerFromRequest(T& ledger, JsonContext& context) return getLedger(ledger, ledgerHash, context); } + if (!indexValue.isConvertibleTo(Json::stringValue)) + return {rpcINVALID_PARAMS, "ledgerIndexMalformed"}; + auto const index = indexValue.asString(); if (index == "current" || index.empty()) @@ -395,11 +398,11 @@ ledgerFromRequest(T& ledger, JsonContext& context) if (index == "closed") return getLedger(ledger, LedgerShortcut::CLOSED, context); - std::uint32_t iVal; - if (beast::lexicalCastChecked(iVal, index)) - return getLedger(ledger, iVal, context); + std::uint32_t val; + if (!beast::lexicalCastChecked(val, index)) + return {rpcINVALID_PARAMS, "ledgerIndexMalformed"}; - return {rpcINVALID_PARAMS, "ledgerIndexMalformed"}; + return getLedger(ledger, val, context); } } // namespace @@ -586,7 +589,7 @@ getLedger(T& ledger, LedgerShortcut shortcut, Context& context) return Status::OK; } -// Explicit instantiaion of above three functions +// Explicit instantiation of above three functions template Status getLedger<>(std::shared_ptr&, uint32_t, Context&); diff --git a/src/xrpld/rpc/handlers/LedgerEntry.cpp b/src/xrpld/rpc/handlers/LedgerEntry.cpp index fb82788907..61a7e2fb2c 100644 --- a/src/xrpld/rpc/handlers/LedgerEntry.cpp +++ b/src/xrpld/rpc/handlers/LedgerEntry.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -38,50 +39,57 @@ namespace ripple { -static std::optional -parseIndex(Json::Value const& params, Json::Value& jvResult) +static Expected +parseObjectID( + Json::Value const& params, + Json::StaticString const fieldName, + std::string const& expectedType = "hex string or object") { - uint256 uNodeIndex; - if (!uNodeIndex.parseHex(params.asString())) + if (auto const uNodeIndex = LedgerEntryHelpers::parse(params)) { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; + return *uNodeIndex; } - - return uNodeIndex; + return LedgerEntryHelpers::invalidFieldError( + "malformedRequest", fieldName, expectedType); } -static std::optional -parseAccountRoot(Json::Value const& params, Json::Value& jvResult) +static Expected +parseIndex(Json::Value const& params, Json::StaticString const fieldName) { - auto const account = parseBase58(params.asString()); - if (!account || account->isZero()) - { - jvResult[jss::error] = "malformedAddress"; - return std::nullopt; - } - - return keylet::account(*account).key; + return parseObjectID(params, fieldName, "hex string"); } -static std::optional -parseAMM(Json::Value const& params, Json::Value& jvResult) +static Expected +parseAccountRoot(Json::Value const& params, Json::StaticString const fieldName) +{ + if (auto const account = LedgerEntryHelpers::parse(params)) + { + return keylet::account(*account).key; + } + + return LedgerEntryHelpers::invalidFieldError( + "malformedAddress", fieldName, "AccountID"); +} + +static Expected +parseAmendments(Json::Value const& params, Json::StaticString const fieldName) +{ + return parseObjectID(params, fieldName, "hex string"); +} + +static Expected +parseAMM(Json::Value const& params, Json::StaticString const fieldName) { if (!params.isObject()) { - uint256 uNodeIndex; - if (!uNodeIndex.parseHex(params.asString())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - return uNodeIndex; + return parseObjectID(params, fieldName); } - if (!params.isMember(jss::asset) || !params.isMember(jss::asset2)) + if (auto const value = + LedgerEntryHelpers::hasRequired(params, {jss::asset, jss::asset2}); + !value) { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; + return Unexpected(value.error()); } try @@ -92,135 +100,136 @@ parseAMM(Json::Value const& params, Json::Value& jvResult) } catch (std::runtime_error const&) { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; + return LedgerEntryHelpers::malformedError("malformedRequest", ""); } } -static std::optional -parseBridge(Json::Value const& params, Json::Value& jvResult) +static Expected +parseBridge(Json::Value const& params, Json::StaticString const fieldName) { - // return the keylet for the specified bridge or nullopt if the - // request is malformed - auto const maybeKeylet = [&]() -> std::optional { - try - { - if (!params.isMember(jss::bridge_account)) - return std::nullopt; - - auto const& jsBridgeAccount = params[jss::bridge_account]; - if (!jsBridgeAccount.isString()) - { - return std::nullopt; - } - - auto const account = - parseBase58(jsBridgeAccount.asString()); - if (!account || account->isZero()) - { - return std::nullopt; - } - - // This may throw and is the reason for the `try` block. The - // try block has a larger scope so the `bridge` variable - // doesn't need to be an optional. - STXChainBridge const bridge(params[jss::bridge]); - STXChainBridge::ChainType const chainType = - STXChainBridge::srcChain(account == bridge.lockingChainDoor()); - - if (account != bridge.door(chainType)) - return std::nullopt; - - return keylet::bridge(bridge, chainType); - } - catch (...) - { - return std::nullopt; - } - }(); - - if (maybeKeylet) + if (!params.isMember(jss::bridge)) { - return maybeKeylet->key; + return Unexpected(LedgerEntryHelpers::missingFieldError(jss::bridge)); } - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; + if (params[jss::bridge].isString()) + { + return parseObjectID(params, fieldName); + } + + auto const bridge = + LedgerEntryHelpers::parseBridgeFields(params[jss::bridge]); + if (!bridge) + return Unexpected(bridge.error()); + + auto const account = LedgerEntryHelpers::requiredAccountID( + params, jss::bridge_account, "malformedBridgeAccount"); + if (!account) + return Unexpected(account.error()); + + STXChainBridge::ChainType const chainType = + STXChainBridge::srcChain(account.value() == bridge->lockingChainDoor()); + if (account.value() != bridge->door(chainType)) + return LedgerEntryHelpers::malformedError("malformedRequest", ""); + + return keylet::bridge(*bridge, chainType).key; } -static std::optional -parseCheck(Json::Value const& params, Json::Value& jvResult) +static Expected +parseCheck(Json::Value const& params, Json::StaticString const fieldName) { - uint256 uNodeIndex; - if (!uNodeIndex.parseHex(params.asString())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - - return uNodeIndex; + return parseObjectID(params, fieldName, "hex string"); } -static std::optional -parseCredential(Json::Value const& cred, Json::Value& jvResult) +static Expected +parseCredential(Json::Value const& cred, Json::StaticString const fieldName) { - if (cred.isString()) + if (!cred.isObject()) { - uint256 uNodeIndex; - if (!uNodeIndex.parseHex(cred.asString())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - return uNodeIndex; + return parseObjectID(cred, fieldName); } - if ((!cred.isMember(jss::subject) || !cred[jss::subject].isString()) || - (!cred.isMember(jss::issuer) || !cred[jss::issuer].isString()) || - (!cred.isMember(jss::credential_type) || - !cred[jss::credential_type].isString())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } + auto const subject = LedgerEntryHelpers::requiredAccountID( + cred, jss::subject, "malformedRequest"); + if (!subject) + return Unexpected(subject.error()); - auto const subject = parseBase58(cred[jss::subject].asString()); - auto const issuer = parseBase58(cred[jss::issuer].asString()); - auto const credType = strUnHex(cred[jss::credential_type].asString()); + auto const issuer = LedgerEntryHelpers::requiredAccountID( + cred, jss::issuer, "malformedRequest"); + if (!issuer) + return Unexpected(issuer.error()); - if (!subject || subject->isZero() || !issuer || issuer->isZero() || - !credType || credType->empty()) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } + auto const credType = LedgerEntryHelpers::requiredHexBlob( + cred, + jss::credential_type, + maxCredentialTypeLength, + "malformedRequest"); + if (!credType) + return Unexpected(credType.error()); return keylet::credential( *subject, *issuer, Slice(credType->data(), credType->size())) .key; } -static STArray +static Expected +parseDelegate(Json::Value const& params, Json::StaticString const fieldName) +{ + if (!params.isObject()) + { + return parseObjectID(params, fieldName); + } + + auto const account = LedgerEntryHelpers::requiredAccountID( + params, jss::account, "malformedAddress"); + if (!account) + return Unexpected(account.error()); + + auto const authorize = LedgerEntryHelpers::requiredAccountID( + params, jss::authorize, "malformedAddress"); + if (!authorize) + return Unexpected(authorize.error()); + + return keylet::delegate(*account, *authorize).key; +} + +static Expected parseAuthorizeCredentials(Json::Value const& jv) { + if (!jv.isArray()) + return LedgerEntryHelpers::invalidFieldError( + "malformedAuthorizedCredentials", + jss::authorized_credentials, + "array"); STArray arr(sfAuthorizeCredentials, jv.size()); for (auto const& jo : jv) { - if (!jo.isObject() || // - !jo.isMember(jss::issuer) || !jo[jss::issuer].isString() || - !jo.isMember(jss::credential_type) || - !jo[jss::credential_type].isString()) - return {}; + if (!jo.isObject()) + return LedgerEntryHelpers::invalidFieldError( + "malformedAuthorizedCredentials", + jss::authorized_credentials, + "array"); + if (auto const value = LedgerEntryHelpers::hasRequired( + jo, + {jss::issuer, jss::credential_type}, + "malformedAuthorizedCredentials"); + !value) + { + return Unexpected(value.error()); + } - auto const issuer = parseBase58(jo[jss::issuer].asString()); - if (!issuer || !*issuer) - return {}; + auto const issuer = LedgerEntryHelpers::requiredAccountID( + jo, jss::issuer, "malformedAuthorizedCredentials"); + if (!issuer) + return Unexpected(issuer.error()); - auto const credentialType = - strUnHex(jo[jss::credential_type].asString()); - if (!credentialType || credentialType->empty() || - credentialType->size() > maxCredentialTypeLength) - return {}; + auto const credentialType = LedgerEntryHelpers::requiredHexBlob( + jo, + jss::credential_type, + maxCredentialTypeLength, + "malformedAuthorizedCredentials"); + if (!credentialType) + return Unexpected(credentialType.error()); auto credential = STObject::makeInnerObject(sfCredential); credential.setAccountID(sfIssuer, *issuer); @@ -231,703 +240,450 @@ parseAuthorizeCredentials(Json::Value const& jv) return arr; } -static std::optional -parseDelegate(Json::Value const& params, Json::Value& jvResult) -{ - if (!params.isObject()) - { - uint256 uNodeIndex; - if (!params.isString() || !uNodeIndex.parseHex(params.asString())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - return uNodeIndex; - } - if (!params.isMember(jss::account) || !params.isMember(jss::authorize)) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - if (!params[jss::account].isString() || !params[jss::authorize].isString()) - { - jvResult[jss::error] = "malformedAddress"; - return std::nullopt; - } - auto const account = - parseBase58(params[jss::account].asString()); - if (!account) - { - jvResult[jss::error] = "malformedAddress"; - return std::nullopt; - } - auto const authorize = - parseBase58(params[jss::authorize].asString()); - if (!authorize) - { - jvResult[jss::error] = "malformedAddress"; - return std::nullopt; - } - return keylet::delegate(*account, *authorize).key; -} - -static std::optional -parseDepositPreauth(Json::Value const& dp, Json::Value& jvResult) +static Expected +parseDepositPreauth(Json::Value const& dp, Json::StaticString const fieldName) { if (!dp.isObject()) { - uint256 uNodeIndex; - if (!dp.isString() || !uNodeIndex.parseHex(dp.asString())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - return uNodeIndex; + return parseObjectID(dp, fieldName); } - // clang-format off - if ( - (!dp.isMember(jss::owner) || !dp[jss::owner].isString()) || - (dp.isMember(jss::authorized) == dp.isMember(jss::authorized_credentials)) || - (dp.isMember(jss::authorized) && !dp[jss::authorized].isString()) || - (dp.isMember(jss::authorized_credentials) && !dp[jss::authorized_credentials].isArray()) - ) - // clang-format on + if ((dp.isMember(jss::authorized) == + dp.isMember(jss::authorized_credentials))) { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; + return LedgerEntryHelpers::malformedError( + "malformedRequest", + "Must have exactly one of `authorized` and " + "`authorized_credentials`."); } - auto const owner = parseBase58(dp[jss::owner].asString()); + auto const owner = + LedgerEntryHelpers::requiredAccountID(dp, jss::owner, "malformedOwner"); if (!owner) { - jvResult[jss::error] = "malformedOwner"; - return std::nullopt; + return Unexpected(owner.error()); } if (dp.isMember(jss::authorized)) { - auto const authorized = - parseBase58(dp[jss::authorized].asString()); - if (!authorized) + if (auto const authorized = + LedgerEntryHelpers::parse(dp[jss::authorized])) { - jvResult[jss::error] = "malformedAuthorized"; - return std::nullopt; + return keylet::depositPreauth(*owner, *authorized).key; } - return keylet::depositPreauth(*owner, *authorized).key; + return LedgerEntryHelpers::invalidFieldError( + "malformedAuthorized", jss::authorized, "AccountID"); } auto const& ac(dp[jss::authorized_credentials]); - STArray const arr = parseAuthorizeCredentials(ac); - - if (arr.empty() || (arr.size() > maxCredentialsArraySize)) + auto const arr = parseAuthorizeCredentials(ac); + if (!arr.has_value()) + return Unexpected(arr.error()); + if (arr->empty() || (arr->size() > maxCredentialsArraySize)) { - jvResult[jss::error] = "malformedAuthorizedCredentials"; - return std::nullopt; + return LedgerEntryHelpers::invalidFieldError( + "malformedAuthorizedCredentials", + jss::authorized_credentials, + "array"); } - auto const& sorted = credentials::makeSorted(arr); + auto const& sorted = credentials::makeSorted(arr.value()); if (sorted.empty()) { - jvResult[jss::error] = "malformedAuthorizedCredentials"; - return std::nullopt; + // TODO: this error message is bad/inaccurate + return LedgerEntryHelpers::invalidFieldError( + "malformedAuthorizedCredentials", + jss::authorized_credentials, + "array"); } - return keylet::depositPreauth(*owner, sorted).key; + return keylet::depositPreauth(*owner, std::move(sorted)).key; } -static std::optional -parseDID(Json::Value const& params, Json::Value& jvResult) +static Expected +parseDID(Json::Value const& params, Json::StaticString const fieldName) { - auto const account = parseBase58(params.asString()); - if (!account || account->isZero()) + auto const account = LedgerEntryHelpers::parse(params); + if (!account) { - jvResult[jss::error] = "malformedAddress"; - return std::nullopt; + return LedgerEntryHelpers::invalidFieldError( + "malformedAddress", fieldName, "AccountID"); } return keylet::did(*account).key; } -static std::optional -parseDirectory(Json::Value const& params, Json::Value& jvResult) +static Expected +parseDirectoryNode( + Json::Value const& params, + Json::StaticString const fieldName) { - if (params.isNull()) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - if (!params.isObject()) { - uint256 uNodeIndex; - if (!uNodeIndex.parseHex(params.asString())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - return uNodeIndex; + return parseObjectID(params, fieldName); } - if (params.isMember(jss::sub_index) && !params[jss::sub_index].isIntegral()) + if (params.isMember(jss::sub_index) && + (!params[jss::sub_index].isConvertibleTo(Json::uintValue) || + params[jss::sub_index].isBool())) { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; + return LedgerEntryHelpers::invalidFieldError( + "malformedRequest", jss::sub_index, "number"); } - std::uint64_t uSubIndex = - params.isMember(jss::sub_index) ? params[jss::sub_index].asUInt() : 0; + if (params.isMember(jss::owner) == params.isMember(jss::dir_root)) + { + return LedgerEntryHelpers::malformedError( + "malformedRequest", + "Must have exactly one of `owner` and `dir_root` fields."); + } + + std::uint64_t uSubIndex = params.get(jss::sub_index, 0).asUInt(); if (params.isMember(jss::dir_root)) { - uint256 uDirRoot; - - if (params.isMember(jss::owner)) + if (auto const uDirRoot = + LedgerEntryHelpers::parse(params[jss::dir_root])) { - // May not specify both dir_root and owner. - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; + return keylet::page(*uDirRoot, uSubIndex).key; } - if (!uDirRoot.parseHex(params[jss::dir_root].asString())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - return keylet::page(uDirRoot, uSubIndex).key; + return LedgerEntryHelpers::invalidFieldError( + "malformedDirRoot", jss::dir_root, "hash"); } if (params.isMember(jss::owner)) { auto const ownerID = - parseBase58(params[jss::owner].asString()); - + LedgerEntryHelpers::parse(params[jss::owner]); if (!ownerID) { - jvResult[jss::error] = "malformedAddress"; - return std::nullopt; + return LedgerEntryHelpers::invalidFieldError( + "malformedAddress", jss::owner, "AccountID"); } return keylet::page(keylet::ownerDir(*ownerID), uSubIndex).key; } - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; + return LedgerEntryHelpers::malformedError("malformedRequest", ""); } -static std::optional -parseEscrow(Json::Value const& params, Json::Value& jvResult) +static Expected +parseEscrow(Json::Value const& params, Json::StaticString const fieldName) { if (!params.isObject()) { - uint256 uNodeIndex; - if (!uNodeIndex.parseHex(params.asString())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - - return uNodeIndex; + return parseObjectID(params, fieldName); } - if (!params.isMember(jss::owner) || !params.isMember(jss::seq) || - !params[jss::seq].isIntegral()) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - - auto const id = parseBase58(params[jss::owner].asString()); - + auto const id = LedgerEntryHelpers::requiredAccountID( + params, jss::owner, "malformedOwner"); if (!id) - { - jvResult[jss::error] = "malformedOwner"; - return std::nullopt; - } + return Unexpected(id.error()); + auto const seq = + LedgerEntryHelpers::requiredUInt32(params, jss::seq, "malformedSeq"); + if (!seq) + return Unexpected(seq.error()); - return keylet::escrow(*id, params[jss::seq].asUInt()).key; + return keylet::escrow(*id, *seq).key; } -static std::optional -parseMPToken(Json::Value const& mptJson, Json::Value& jvResult) +static Expected +parseFeeSettings(Json::Value const& params, Json::StaticString const fieldName) { - if (!mptJson.isObject()) - { - uint256 uNodeIndex; - if (!uNodeIndex.parseHex(mptJson.asString())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - return uNodeIndex; - } - - if (!mptJson.isMember(jss::mpt_issuance_id) || - !mptJson.isMember(jss::account)) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - - try - { - auto const mptIssuanceIdStr = mptJson[jss::mpt_issuance_id].asString(); - - uint192 mptIssuanceID; - if (!mptIssuanceID.parseHex(mptIssuanceIdStr)) - Throw("Cannot parse mpt_issuance_id"); - - auto const account = - parseBase58(mptJson[jss::account].asString()); - - if (!account || account->isZero()) - { - jvResult[jss::error] = "malformedAddress"; - return std::nullopt; - } - - return keylet::mptoken(mptIssuanceID, *account).key; - } - catch (std::runtime_error const&) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } + return parseObjectID(params, fieldName, "hex string"); } -static std::optional +static Expected +parseLedgerHashes(Json::Value const& params, Json::StaticString const fieldName) +{ + return parseObjectID(params, fieldName, "hex string"); +} + +static Expected +parseMPToken(Json::Value const& params, Json::StaticString const fieldName) +{ + if (!params.isObject()) + { + return parseObjectID(params, fieldName); + } + + auto const mptIssuanceID = LedgerEntryHelpers::requiredUInt192( + params, jss::mpt_issuance_id, "malformedMPTIssuanceID"); + if (!mptIssuanceID) + return Unexpected(mptIssuanceID.error()); + + auto const account = LedgerEntryHelpers::requiredAccountID( + params, jss::account, "malformedAccount"); + if (!account) + return Unexpected(account.error()); + + return keylet::mptoken(*mptIssuanceID, *account).key; +} + +static Expected parseMPTokenIssuance( - Json::Value const& unparsedMPTIssuanceID, - Json::Value& jvResult) + Json::Value const& params, + Json::StaticString const fieldName) { - if (unparsedMPTIssuanceID.isString()) - { - uint192 mptIssuanceID; - if (!mptIssuanceID.parseHex(unparsedMPTIssuanceID.asString())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } + auto const mptIssuanceID = LedgerEntryHelpers::parse(params); + if (!mptIssuanceID) + return LedgerEntryHelpers::invalidFieldError( + "malformedMPTokenIssuance", fieldName, "Hash192"); - return keylet::mptIssuance(mptIssuanceID).key; - } - - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; + return keylet::mptIssuance(*mptIssuanceID).key; } -static std::optional -parseNFTokenPage(Json::Value const& params, Json::Value& jvResult) +static Expected +parseNFTokenOffer(Json::Value const& params, Json::StaticString const fieldName) { - if (params.isString()) - { - uint256 uNodeIndex; - if (!uNodeIndex.parseHex(params.asString())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - return uNodeIndex; - } - - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; + return parseObjectID(params, fieldName, "hex string"); } -static std::optional -parseOffer(Json::Value const& params, Json::Value& jvResult) +static Expected +parseNFTokenPage(Json::Value const& params, Json::StaticString const fieldName) +{ + return parseObjectID(params, fieldName, "hex string"); +} + +static Expected +parseNegativeUNL(Json::Value const& params, Json::StaticString const fieldName) +{ + return parseObjectID(params, fieldName, "hex string"); +} + +static Expected +parseOffer(Json::Value const& params, Json::StaticString const fieldName) { if (!params.isObject()) { - uint256 uNodeIndex; - if (!uNodeIndex.parseHex(params.asString())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - return uNodeIndex; + return parseObjectID(params, fieldName); } - if (!params.isMember(jss::account) || !params.isMember(jss::seq) || - !params[jss::seq].isIntegral()) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - - auto const id = parseBase58(params[jss::account].asString()); + auto const id = LedgerEntryHelpers::requiredAccountID( + params, jss::account, "malformedAddress"); if (!id) - { - jvResult[jss::error] = "malformedAddress"; - return std::nullopt; - } + return Unexpected(id.error()); - return keylet::offer(*id, params[jss::seq].asUInt()).key; + auto const seq = LedgerEntryHelpers::requiredUInt32( + params, jss::seq, "malformedRequest"); + if (!seq) + return Unexpected(seq.error()); + + return keylet::offer(*id, *seq).key; } -static std::optional -parseOracle(Json::Value const& params, Json::Value& jvResult) +static Expected +parseOracle(Json::Value const& params, Json::StaticString const fieldName) { if (!params.isObject()) { - uint256 uNodeIndex; - if (!uNodeIndex.parseHex(params.asString())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - return uNodeIndex; + return parseObjectID(params, fieldName); } - if (!params.isMember(jss::oracle_document_id) || - !params.isMember(jss::account)) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } + auto const id = LedgerEntryHelpers::requiredAccountID( + params, jss::account, "malformedAccount"); + if (!id) + return Unexpected(id.error()); - auto const& oracle = params; - auto const documentID = [&]() -> std::optional { - auto const id = oracle[jss::oracle_document_id]; - if (id.isUInt() || (id.isInt() && id.asInt() >= 0)) - return std::make_optional(id.asUInt()); + auto const seq = LedgerEntryHelpers::requiredUInt32( + params, jss::oracle_document_id, "malformedDocumentID"); + if (!seq) + return Unexpected(seq.error()); - if (id.isString()) - { - std::uint32_t v; - if (beast::lexicalCastChecked(v, id.asString())) - return std::make_optional(v); - } - - return std::nullopt; - }(); - - auto const account = - parseBase58(oracle[jss::account].asString()); - if (!account || account->isZero()) - { - jvResult[jss::error] = "malformedAddress"; - return std::nullopt; - } - - if (!documentID) - { - jvResult[jss::error] = "malformedDocumentID"; - return std::nullopt; - } - - return keylet::oracle(*account, *documentID).key; + return keylet::oracle(*id, *seq).key; } -static std::optional -parsePaymentChannel(Json::Value const& params, Json::Value& jvResult) +static Expected +parsePayChannel(Json::Value const& params, Json::StaticString const fieldName) { - uint256 uNodeIndex; - if (!uNodeIndex.parseHex(params.asString())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - - return uNodeIndex; + return parseObjectID(params, fieldName, "hex string"); } -static std::optional -parsePermissionedDomains(Json::Value const& pd, Json::Value& jvResult) +static Expected +parsePermissionedDomain( + Json::Value const& pd, + Json::StaticString const fieldName) { if (pd.isString()) { - auto const index = parseIndex(pd, jvResult); - return index; + return parseObjectID(pd, fieldName); } if (!pd.isObject()) { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; + return LedgerEntryHelpers::invalidFieldError( + "malformedRequest", fieldName, "hex string or object"); } - if (!pd.isMember(jss::account)) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - - if (!pd[jss::account].isString()) - { - jvResult[jss::error] = "malformedAddress"; - return std::nullopt; - } - - if (!pd.isMember(jss::seq) || - (pd[jss::seq].isInt() && pd[jss::seq].asInt() < 0) || - (!pd[jss::seq].isInt() && !pd[jss::seq].isUInt())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - - auto const account = parseBase58(pd[jss::account].asString()); + auto const account = LedgerEntryHelpers::requiredAccountID( + pd, jss::account, "malformedAddress"); if (!account) - { - jvResult[jss::error] = "malformedAddress"; - return std::nullopt; - } + return Unexpected(account.error()); + + auto const seq = + LedgerEntryHelpers::requiredUInt32(pd, jss::seq, "malformedRequest"); + if (!seq) + return Unexpected(seq.error()); return keylet::permissionedDomain(*account, pd[jss::seq].asUInt()).key; } -static std::optional -parseRippleState(Json::Value const& jvRippleState, Json::Value& jvResult) +static Expected +parseRippleState( + Json::Value const& jvRippleState, + Json::StaticString const fieldName) { Currency uCurrency; - if (!jvRippleState.isObject() || !jvRippleState.isMember(jss::currency) || - !jvRippleState.isMember(jss::accounts) || - !jvRippleState[jss::accounts].isArray() || - 2 != jvRippleState[jss::accounts].size() || - !jvRippleState[jss::accounts][0u].isString() || - !jvRippleState[jss::accounts][1u].isString() || - (jvRippleState[jss::accounts][0u].asString() == - jvRippleState[jss::accounts][1u].asString())) + if (!jvRippleState.isObject()) { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; + return parseObjectID(jvRippleState, fieldName); + } + + if (auto const value = LedgerEntryHelpers::hasRequired( + jvRippleState, {jss::currency, jss::accounts}); + !value) + { + return Unexpected(value.error()); + } + + if (!jvRippleState[jss::accounts].isArray() || + jvRippleState[jss::accounts].size() != 2) + { + return LedgerEntryHelpers::invalidFieldError( + "malformedRequest", jss::accounts, "length-2 array of Accounts"); } auto const id1 = - parseBase58(jvRippleState[jss::accounts][0u].asString()); + LedgerEntryHelpers::parse(jvRippleState[jss::accounts][0u]); auto const id2 = - parseBase58(jvRippleState[jss::accounts][1u].asString()); + LedgerEntryHelpers::parse(jvRippleState[jss::accounts][1u]); if (!id1 || !id2) { - jvResult[jss::error] = "malformedAddress"; - return std::nullopt; + return LedgerEntryHelpers::invalidFieldError( + "malformedAddress", jss::accounts, "array of Accounts"); + } + if (id1 == id2) + { + return LedgerEntryHelpers::malformedError( + "malformedRequest", "Cannot have a trustline to self."); } - if (!to_currency(uCurrency, jvRippleState[jss::currency].asString())) + if (!jvRippleState[jss::currency].isString() || + jvRippleState[jss::currency] == "" || + !to_currency(uCurrency, jvRippleState[jss::currency].asString())) { - jvResult[jss::error] = "malformedCurrency"; - return std::nullopt; + return LedgerEntryHelpers::invalidFieldError( + "malformedCurrency", jss::currency, "Currency"); } return keylet::line(*id1, *id2, uCurrency).key; } -static std::optional -parseTicket(Json::Value const& params, Json::Value& jvResult) +static Expected +parseSignerList(Json::Value const& params, Json::StaticString const fieldName) +{ + return parseObjectID(params, fieldName, "hex string"); +} + +static Expected +parseTicket(Json::Value const& params, Json::StaticString const fieldName) { if (!params.isObject()) { - uint256 uNodeIndex; - if (!uNodeIndex.parseHex(params.asString())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - return uNodeIndex; + return parseObjectID(params, fieldName); } - if (!params.isMember(jss::account) || !params.isMember(jss::ticket_seq) || - !params[jss::ticket_seq].isIntegral()) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - - auto const id = parseBase58(params[jss::account].asString()); + auto const id = LedgerEntryHelpers::requiredAccountID( + params, jss::account, "malformedAddress"); if (!id) - { - jvResult[jss::error] = "malformedAddress"; - return std::nullopt; - } + return Unexpected(id.error()); - return getTicketIndex(*id, params[jss::ticket_seq].asUInt()); + auto const seq = LedgerEntryHelpers::requiredUInt32( + params, jss::ticket_seq, "malformedRequest"); + if (!seq) + return Unexpected(seq.error()); + + return getTicketIndex(*id, *seq); } -static std::optional -parseVault(Json::Value const& params, Json::Value& jvResult) +static Expected +parseVault(Json::Value const& params, Json::StaticString const fieldName) { if (!params.isObject()) { - uint256 uNodeIndex; - if (!uNodeIndex.parseHex(params.asString())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - return uNodeIndex; + return parseObjectID(params, fieldName); } - if (!params.isMember(jss::owner) || !params.isMember(jss::seq) || - !(params[jss::seq].isInt() || params[jss::seq].isUInt()) || - params[jss::seq].asDouble() <= 0.0 || - params[jss::seq].asDouble() > double(Json::Value::maxUInt)) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - - auto const id = parseBase58(params[jss::owner].asString()); + auto const id = LedgerEntryHelpers::requiredAccountID( + params, jss::owner, "malformedOwner"); if (!id) - { - jvResult[jss::error] = "malformedOwner"; - return std::nullopt; - } + return Unexpected(id.error()); - return keylet::vault(*id, params[jss::seq].asUInt()).key; + auto const seq = LedgerEntryHelpers::requiredUInt32( + params, jss::seq, "malformedRequest"); + if (!seq) + return Unexpected(seq.error()); + + return keylet::vault(*id, *seq).key; } -static std::optional -parseXChainOwnedClaimID(Json::Value const& claim_id, Json::Value& jvResult) +static Expected +parseXChainOwnedClaimID( + Json::Value const& claim_id, + Json::StaticString const fieldName) { - if (claim_id.isString()) + if (!claim_id.isObject()) { - uint256 uNodeIndex; - // we accept a node id as specifier of a xchain claim id - if (!uNodeIndex.parseHex(claim_id.asString())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - return uNodeIndex; + return parseObjectID(claim_id, fieldName); } - if (!claim_id.isObject() || - !(claim_id.isMember(sfIssuingChainDoor.getJsonName()) && - claim_id[sfIssuingChainDoor.getJsonName()].isString()) || - !(claim_id.isMember(sfLockingChainDoor.getJsonName()) && - claim_id[sfLockingChainDoor.getJsonName()].isString()) || - !claim_id.isMember(sfIssuingChainIssue.getJsonName()) || - !claim_id.isMember(sfLockingChainIssue.getJsonName()) || - !claim_id.isMember(jss::xchain_owned_claim_id)) + auto const bridge_spec = LedgerEntryHelpers::parseBridgeFields(claim_id); + if (!bridge_spec) + return Unexpected(bridge_spec.error()); + + auto const seq = LedgerEntryHelpers::requiredUInt32( + claim_id, jss::xchain_owned_claim_id, "malformedXChainOwnedClaimID"); + if (!seq) { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; + return Unexpected(seq.error()); } - // if not specified with a node id, a claim_id is specified by - // four strings defining the bridge (locking_chain_door, - // locking_chain_issue, issuing_chain_door, issuing_chain_issue) - // and the claim id sequence number. - auto const lockingChainDoor = parseBase58( - claim_id[sfLockingChainDoor.getJsonName()].asString()); - auto const issuingChainDoor = parseBase58( - claim_id[sfIssuingChainDoor.getJsonName()].asString()); - Issue lockingChainIssue, issuingChainIssue; - bool valid = lockingChainDoor && issuingChainDoor; - - if (valid) - { - try - { - lockingChainIssue = - issueFromJson(claim_id[sfLockingChainIssue.getJsonName()]); - issuingChainIssue = - issueFromJson(claim_id[sfIssuingChainIssue.getJsonName()]); - } - catch (std::runtime_error const& ex) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - } - - if (valid && claim_id[jss::xchain_owned_claim_id].isIntegral()) - { - auto const seq = claim_id[jss::xchain_owned_claim_id].asUInt(); - - STXChainBridge bridge_spec( - *lockingChainDoor, - lockingChainIssue, - *issuingChainDoor, - issuingChainIssue); - Keylet keylet = keylet::xChainClaimID(bridge_spec, seq); - return keylet.key; - } - - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; + Keylet keylet = keylet::xChainClaimID(*bridge_spec, *seq); + return keylet.key; } -static std::optional +static Expected parseXChainOwnedCreateAccountClaimID( Json::Value const& claim_id, - Json::Value& jvResult) + Json::StaticString const fieldName) { - if (claim_id.isString()) + if (!claim_id.isObject()) { - uint256 uNodeIndex; - // we accept a node id as specifier of a xchain create account - // claim_id - if (!uNodeIndex.parseHex(claim_id.asString())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - return uNodeIndex; + return parseObjectID(claim_id, fieldName); } - if (!claim_id.isObject() || - !(claim_id.isMember(sfIssuingChainDoor.getJsonName()) && - claim_id[sfIssuingChainDoor.getJsonName()].isString()) || - !(claim_id.isMember(sfLockingChainDoor.getJsonName()) && - claim_id[sfLockingChainDoor.getJsonName()].isString()) || - !claim_id.isMember(sfIssuingChainIssue.getJsonName()) || - !claim_id.isMember(sfLockingChainIssue.getJsonName()) || - !claim_id.isMember(jss::xchain_owned_create_account_claim_id)) + auto const bridge_spec = LedgerEntryHelpers::parseBridgeFields(claim_id); + if (!bridge_spec) + return Unexpected(bridge_spec.error()); + + auto const seq = LedgerEntryHelpers::requiredUInt32( + claim_id, + jss::xchain_owned_create_account_claim_id, + "malformedXChainOwnedCreateAccountClaimID"); + if (!seq) { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; + return Unexpected(seq.error()); } - // if not specified with a node id, a create account claim_id is - // specified by four strings defining the bridge - // (locking_chain_door, locking_chain_issue, issuing_chain_door, - // issuing_chain_issue) and the create account claim id sequence - // number. - auto const lockingChainDoor = parseBase58( - claim_id[sfLockingChainDoor.getJsonName()].asString()); - auto const issuingChainDoor = parseBase58( - claim_id[sfIssuingChainDoor.getJsonName()].asString()); - Issue lockingChainIssue, issuingChainIssue; - bool valid = lockingChainDoor && issuingChainDoor; - if (valid) - { - try - { - lockingChainIssue = - issueFromJson(claim_id[sfLockingChainIssue.getJsonName()]); - issuingChainIssue = - issueFromJson(claim_id[sfIssuingChainIssue.getJsonName()]); - } - catch (std::runtime_error const& ex) - { - valid = false; - jvResult[jss::error] = "malformedRequest"; - } - } - - if (valid && - claim_id[jss::xchain_owned_create_account_claim_id].isIntegral()) - { - auto const seq = - claim_id[jss::xchain_owned_create_account_claim_id].asUInt(); - - STXChainBridge bridge_spec( - *lockingChainDoor, - lockingChainIssue, - *issuingChainDoor, - issuingChainIssue); - Keylet keylet = keylet::xChainCreateAccountClaimID(bridge_spec, seq); - return keylet.key; - } - - return std::nullopt; + Keylet keylet = keylet::xChainCreateAccountClaimID(*bridge_spec, *seq); + return keylet.key; } -using FunctionType = - std::function(Json::Value const&, Json::Value&)>; +using FunctionType = Expected (*)( + Json::Value const&, + Json::StaticString const); struct LedgerEntry { @@ -944,50 +700,49 @@ struct LedgerEntry Json::Value doLedgerEntry(RPC::JsonContext& context) { + static auto ledgerEntryParsers = std::to_array({ +#pragma push_macro("LEDGER_ENTRY") +#undef LEDGER_ENTRY + +#define LEDGER_ENTRY(tag, value, name, rpcName, fields) \ + {jss::rpcName, parse##name, tag}, + +#include + +#undef LEDGER_ENTRY +#pragma pop_macro("LEDGER_ENTRY") + {jss::index, parseIndex, ltANY}, + // aliases + {jss::account_root, parseAccountRoot, ltACCOUNT_ROOT}, + {jss::ripple_state, parseRippleState, ltRIPPLE_STATE}, + }); + + auto hasMoreThanOneMember = [&]() { + int count = 0; + + for (auto const& ledgerEntry : ledgerEntryParsers) + { + if (context.params.isMember(ledgerEntry.fieldName)) + { + count++; + if (count > 1) // Early exit if more than one is found + return true; + } + } + return false; // Return false if <= 1 is found + }(); + + if (hasMoreThanOneMember) + { + return RPC::make_param_error("Too many fields provided."); + } + std::shared_ptr lpLedger; auto jvResult = RPC::lookupLedger(lpLedger, context); if (!lpLedger) return jvResult; - static auto ledgerEntryParsers = std::to_array({ - {jss::index, parseIndex, ltANY}, - {jss::account_root, parseAccountRoot, ltACCOUNT_ROOT}, - // TODO: add amendments - {jss::amm, parseAMM, ltAMM}, - {jss::bridge, parseBridge, ltBRIDGE}, - {jss::check, parseCheck, ltCHECK}, - {jss::credential, parseCredential, ltCREDENTIAL}, - {jss::delegate, parseDelegate, ltDELEGATE}, - {jss::deposit_preauth, parseDepositPreauth, ltDEPOSIT_PREAUTH}, - {jss::did, parseDID, ltDID}, - {jss::directory, parseDirectory, ltDIR_NODE}, - {jss::escrow, parseEscrow, ltESCROW}, - // TODO: add fee, hashes - {jss::mpt_issuance, parseMPTokenIssuance, ltMPTOKEN_ISSUANCE}, - {jss::mptoken, parseMPToken, ltMPTOKEN}, - // TODO: add NFT Offers - {jss::nft_page, parseNFTokenPage, ltNFTOKEN_PAGE}, - // TODO: add NegativeUNL - {jss::offer, parseOffer, ltOFFER}, - {jss::oracle, parseOracle, ltORACLE}, - {jss::payment_channel, parsePaymentChannel, ltPAYCHAN}, - {jss::permissioned_domain, - parsePermissionedDomains, - ltPERMISSIONED_DOMAIN}, - {jss::ripple_state, parseRippleState, ltRIPPLE_STATE}, - // This is an alias, since the `ledger_data` filter uses jss::state - {jss::state, parseRippleState, ltRIPPLE_STATE}, - {jss::ticket, parseTicket, ltTICKET}, - {jss::xchain_owned_claim_id, - parseXChainOwnedClaimID, - ltXCHAIN_OWNED_CLAIM_ID}, - {jss::xchain_owned_create_account_claim_id, - parseXChainOwnedCreateAccountClaimID, - ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID}, - {jss::vault, parseVault, ltVAULT}, - }); - uint256 uNodeIndex; LedgerEntryType expectedType = ltANY; @@ -1006,34 +761,33 @@ doLedgerEntry(RPC::JsonContext& context) Json::Value const& params = ledgerEntry.fieldName == jss::bridge ? context.params : context.params[ledgerEntry.fieldName]; - uNodeIndex = ledgerEntry.parseFunction(params, jvResult) - .value_or(beast::zero); - if (jvResult.isMember(jss::error)) - { - return jvResult; - } + auto const result = + ledgerEntry.parseFunction(params, ledgerEntry.fieldName); + if (!result) + return result.error(); + + uNodeIndex = result.value(); found = true; break; } } - if (!found) { if (context.apiVersion < 2u) + { jvResult[jss::error] = "unknownOption"; - else - jvResult[jss::error] = "invalidParams"; - return jvResult; + return jvResult; + } + return RPC::make_param_error("No ledger_entry params provided."); } } catch (Json::error& e) { if (context.apiVersion > 1u) { - // For apiVersion 2 onwards, any parsing failures that throw this - // exception return an invalidParam error. - jvResult[jss::error] = "invalidParams"; - return jvResult; + // For apiVersion 2 onwards, any parsing failures that throw + // this exception return an invalidParam error. + return RPC::make_error(rpcINVALID_PARAMS); } else throw; @@ -1041,8 +795,7 @@ doLedgerEntry(RPC::JsonContext& context) if (uNodeIndex.isZero()) { - jvResult[jss::error] = "entryNotFound"; - return jvResult; + return RPC::make_error(rpcENTRY_NOT_FOUND); } auto const sleNode = lpLedger->read(keylet::unchecked(uNodeIndex)); @@ -1054,14 +807,12 @@ doLedgerEntry(RPC::JsonContext& context) if (!sleNode) { // Not found. - jvResult[jss::error] = "entryNotFound"; - return jvResult; + return RPC::make_error(rpcENTRY_NOT_FOUND); } if ((expectedType != ltANY) && (expectedType != sleNode->getType())) { - jvResult[jss::error] = "unexpectedLedgerType"; - return jvResult; + return RPC::make_error(rpcUNEXPECTED_LEDGER_TYPE); } if (bNodeBinary) @@ -1091,7 +842,7 @@ doLedgerEntryGrpc( grpc::Status status = grpc::Status::OK; std::shared_ptr ledger; - if (auto const status = RPC::ledgerFromRequest(ledger, context)) + if (auto status = RPC::ledgerFromRequest(ledger, context)) { grpc::Status errorStatus; if (status.toErrorCode() == rpcINVALID_PARAMS) diff --git a/src/xrpld/rpc/handlers/LedgerEntryHelpers.h b/src/xrpld/rpc/handlers/LedgerEntryHelpers.h new file mode 100644 index 0000000000..12b99dbbff --- /dev/null +++ b/src/xrpld/rpc/handlers/LedgerEntryHelpers.h @@ -0,0 +1,299 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012-2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace ripple { + +namespace LedgerEntryHelpers { + +Unexpected +missingFieldError( + Json::StaticString const field, + std::optional err = std::nullopt) +{ + Json::Value json = Json::objectValue; + auto error = RPC::missing_field_message(std::string(field.c_str())); + json[jss::error] = err.value_or("malformedRequest"); + json[jss::error_code] = rpcINVALID_PARAMS; + json[jss::error_message] = std::move(error); + return Unexpected(json); +} + +Unexpected +invalidFieldError( + std::string const& err, + Json::StaticString const field, + std::string const& type) +{ + Json::Value json = Json::objectValue; + auto error = RPC::expected_field_message(field, type); + json[jss::error] = err; + json[jss::error_code] = rpcINVALID_PARAMS; + json[jss::error_message] = std::move(error); + return Unexpected(json); +} + +Unexpected +malformedError(std::string const& err, std::string const& message) +{ + Json::Value json = Json::objectValue; + json[jss::error] = err; + json[jss::error_code] = rpcINVALID_PARAMS; + json[jss::error_message] = message; + return Unexpected(json); +} + +Expected +hasRequired( + Json::Value const& params, + std::initializer_list fields, + std::optional err = std::nullopt) +{ + for (auto const field : fields) + { + if (!params.isMember(field) || params[field].isNull()) + { + return missingFieldError(field, err); + } + } + return true; +} + +template +std::optional +parse(Json::Value const& param); + +template +Expected +required( + Json::Value const& params, + Json::StaticString const fieldName, + std::string const& err, + std::string const& expectedType) +{ + if (!params.isMember(fieldName) || params[fieldName].isNull()) + { + return missingFieldError(fieldName); + } + if (auto obj = parse(params[fieldName])) + { + return *obj; + } + return invalidFieldError(err, fieldName, expectedType); +} + +template <> +std::optional +parse(Json::Value const& param) +{ + if (!param.isString()) + return std::nullopt; + + auto const account = parseBase58(param.asString()); + if (!account || account->isZero()) + { + return std::nullopt; + } + + return account; +} + +Expected +requiredAccountID( + Json::Value const& params, + Json::StaticString const fieldName, + std::string const& err) +{ + return required(params, fieldName, err, "AccountID"); +} + +std::optional +parseHexBlob(Json::Value const& param, std::size_t maxLength) +{ + if (!param.isString()) + return std::nullopt; + + auto const blob = strUnHex(param.asString()); + if (!blob || blob->empty() || blob->size() > maxLength) + return std::nullopt; + + return blob; +} + +Expected +requiredHexBlob( + Json::Value const& params, + Json::StaticString const fieldName, + std::size_t maxLength, + std::string const& err) +{ + if (!params.isMember(fieldName) || params[fieldName].isNull()) + { + return missingFieldError(fieldName); + } + if (auto blob = parseHexBlob(params[fieldName], maxLength)) + { + return *blob; + } + return invalidFieldError(err, fieldName, "hex string"); +} + +template <> +std::optional +parse(Json::Value const& param) +{ + if (param.isUInt() || (param.isInt() && param.asInt() >= 0)) + return param.asUInt(); + + if (param.isString()) + { + std::uint32_t v; + if (beast::lexicalCastChecked(v, param.asString())) + return v; + } + + return std::nullopt; +} + +Expected +requiredUInt32( + Json::Value const& params, + Json::StaticString const fieldName, + std::string const& err) +{ + return required(params, fieldName, err, "number"); +} + +template <> +std::optional +parse(Json::Value const& param) +{ + uint256 uNodeIndex; + if (!param.isString() || !uNodeIndex.parseHex(param.asString())) + { + return std::nullopt; + } + + return uNodeIndex; +} + +Expected +requiredUInt256( + Json::Value const& params, + Json::StaticString const fieldName, + std::string const& err) +{ + return required(params, fieldName, err, "Hash256"); +} + +template <> +std::optional +parse(Json::Value const& param) +{ + uint192 field; + if (!param.isString() || !field.parseHex(param.asString())) + { + return std::nullopt; + } + + return field; +} + +Expected +requiredUInt192( + Json::Value const& params, + Json::StaticString const fieldName, + std::string const& err) +{ + return required(params, fieldName, err, "Hash192"); +} + +Expected +parseBridgeFields(Json::Value const& params) +{ + if (auto const value = hasRequired( + params, + {jss::LockingChainDoor, + jss::LockingChainIssue, + jss::IssuingChainDoor, + jss::IssuingChainIssue}); + !value) + { + return Unexpected(value.error()); + } + + auto const lockingChainDoor = requiredAccountID( + params, jss::LockingChainDoor, "malformedLockingChainDoor"); + if (!lockingChainDoor) + { + return Unexpected(lockingChainDoor.error()); + } + + auto const issuingChainDoor = requiredAccountID( + params, jss::IssuingChainDoor, "malformedIssuingChainDoor"); + if (!issuingChainDoor) + { + return Unexpected(issuingChainDoor.error()); + } + + Issue lockingChainIssue; + try + { + lockingChainIssue = issueFromJson(params[jss::LockingChainIssue]); + } + catch (std::runtime_error const& ex) + { + return invalidFieldError( + "malformedIssue", jss::LockingChainIssue, "Issue"); + } + + Issue issuingChainIssue; + try + { + issuingChainIssue = issueFromJson(params[jss::IssuingChainIssue]); + } + catch (std::runtime_error const& ex) + { + return invalidFieldError( + "malformedIssue", jss::IssuingChainIssue, "Issue"); + } + + return STXChainBridge( + *lockingChainDoor, + lockingChainIssue, + *issuingChainDoor, + issuingChainIssue); +} + +} // namespace LedgerEntryHelpers + +} // namespace ripple