From e919a25ecb1b5b3fd1a5dad2e0b56c89e1b7835b Mon Sep 17 00:00:00 2001 From: Shawn Xie <35279399+shawnxie999@users.noreply.github.com> Date: Tue, 7 Oct 2025 14:14:34 -0400 Subject: [PATCH] Merge develop into ripple/confidential-transfer (#5835) * Fix: Don't flag consensus as stalled prematurely (#5658) Fix stalled consensus detection to prevent false positives in situations where there are no disputed transactions. Stalled consensus detection was added to 2.5.0 in response to a network consensus halt that caused a round to run for over an hour. However, it has a flaw that makes it very easy to have false positives. Those false positives are usually mitigated by other checks that prevent them from having an effect, but there have been several instances of validators "running ahead" because there are circumstances where the other checks are "successful", allowing the stall state to be checked. * Set version to 2.5.1 * fix: Skip processing transaction batch if the batch is empty (#5670) Avoids an assertion failure in NetworkOPsImp::apply in the unlikely event that all incoming transactions are invalid. * Fix: EscrowTokenV1 (#5571) * resolves an accounting inconsistency in MPT escrows where transfer fees were not properly handled when unlocking escrowed tokens. * refactor: Wrap GitHub CI conditionals in curly braces (#5796) This change wraps all GitHub conditionals in `${{ .. }}`, both for consistency and to reduce unexpected failures, because it was previously noticed that not all conditionals work without those curly braces. * Only notify clio for PRs targeting the release and master branches (#5794) Clio should only be notified when releases are about to be made, instead of for all PR, so this change only notifies Clio when a PR targets the release or master branch. * Support DynamicMPT XLS-94d (#5705) * extends the functionality of the MPTokenIssuanceSet transaction, allowing the issuer to update fields or flags that were explicitly marked as mutable during creation. * Bugfix: Adds graceful peer disconnection (#5669) The XRPL establishes connections in three stages: first a TCP connection, then a TLS/SSL handshake to secure the connection, and finally an upgrade to the bespoke XRP Ledger peer-to-peer protocol. During connection termination, xrpld directly closes the TCP connection, bypassing the TLS/SSL shutdown handshake. This makes peer disconnection diagnostics more difficult - abrupt TCP termination appears as if the peer crashed rather than disconnected gracefully. This change refactors the connection lifecycle with the following changes: - Enhanced outgoing connection logic with granular timeouts for each connection stage (TCP, TLS, XRPL handshake) to improve diagnostic capabilities - Updated both PeerImp and ConnectAttempt to use proper asynchronous TLS shutdown procedures for graceful connection termination * Downgrade to boost 1.83 * Set version to 2.6.1-rc1 * chore: Use self hosted windows runners (#5780) This changes switches from the GitHub-managed Windows runners to self-hosted runners to significantly reduce build time. * Rename mutable flags (#5797) This is a minor change on top of #5705 * fix(amendment): Add missing fields for keylets to ledger objects (#5646) This change adds a fix amendment (`fixIncludeKeyletFields`) that adds: * `sfSequence` to `Escrow` and `PayChannel` * `sfOwner` to `SignerList` * `sfOracleDocumentID` to `Oracle` This ensures that all ledger entries hold all the information needed to determine their keylet. * chore: Limits CI build and test parallelism to reduce resource contention (#5799) GitHub runners have a limit on how many concurrent jobs they can actually process (even though they will try to run them all at the same time), and similarly the Conan remote cannot handle hundreds of concurrent requests. Previously, the Conan dependency uploading was already limited to max 10 jobs running in parallel, and this change makes the same change to the build+test workflow. * chore: Build and test all configs for daily scheduled run (#5801) This change re-enables building and testing all configurations, but only for the daily scheduled run. Previously all configurations were run for each merge into the develop branch, but that overwhelmed both the GitHub runners and the Conan remote, and thus they were limited to just a subset of configurations. Now that the number of jobs is limited via `max-parallel: 10`, we should be able to safely enable building all configurations again. However, building them all once a day instead of for each PR merge should be sufficient. * chore: Add unit tests dir to code coverage excludes (#5803) This change excludes unit test code from code coverage reporting. * refactor: Modularise ledger (#5493) This change moves the ledger code to libxrpl. * Mark PermissionDelegation as unsupported * Set version to 2.6.1-rc2 * Miscellaneous refactors and updates (#5590) - Added a new Invariant: `ValidPseudoAccounts` which checks that all pseudo-accounts behave consistently through creation and updates, and that no "real" accounts look like pseudo-accounts (which means they don't have a 0 sequence). - `to_short_string(base_uint)`. Like `to_string`, but only returns the first 8 characters. (Similar to how a git commit ID can be abbreviated.) Used as a wrapped sink to prefix most transaction-related messages. More can be added later. - `XRPL_ASSERT_PARTS`. Convenience wrapper for `XRPL_ASSERT`, which takes the `function` and `description` as separate parameters. - `SField::sMD_PseudoAccount`. Metadata option for `SField` definitions to indicate that the field, if set in an `AccountRoot` indicates that account is a pseudo-account. Removes the need for hard-coded field lists all over the place. Added the flag to `AMMID` and `VaultID`. - Added functionality to `SField` ctor to detect both code and name collisions using asserts. And require all SFields to have a name - Convenience type aliases `STLedgerEntry::const_pointer` and `STLedgerEntry::const_ref`. (`SLE` is an alias to `STLedgerEntry`.) - Generalized `feeunit.h` (`TaggedFee`) into `unit.h` (`ValueUnit`) and added new "BIPS"-related tags for future use. Also refactored the type restrictions to use Concepts. - Restructured `transactions.macro` to do two big things 1. Include the `#include` directives for transactor header files directly in the macro file. Removes the need to update `applySteps.cpp` and the resulting conflicts. 2. Added a `privileges` parameter to the `TRANSACTION` macro, which specifies some of the operations a transaction is allowed to do. These `privileges` are enforced by invariant checks. Again, removed the need to update scattered lists of transaction types in various checks. - Unit tests: 1. Moved more helper functions into `TestHelpers.h` and `.cpp`. 2. Cleaned up the namespaces to prevent / mitigate random collisions and ambiguous symbols, particularly in unity builds. 3. Generalized `Env::balance` to add support for `MPTIssue` and `Asset`. 4. Added a set of helper classes to simplify `Env` transaction parameter classes: `JTxField`, `JTxFieldWrapper`, and a bunch of classes derived or aliased from it. For an example of how awesome it is, check the changes `src/test/jtx/escrow.h` for how much simpler the definitions are for `finish_time`, `cancel_time`, `condition`, and `fulfillment`. 5. Generalized several of the amount-related helper classes to understand `Asset`s. 6. `env.balance` for an MPT issuer will return a negative number (or 0) for consistency with IOUs. * refactor: Simplify STParsedJSON with some helper functions (#5591) - Add code coverage for STParsedJSON edge cases Co-authored-by: Denis Angell * test: Add STInteger and STParsedJSON tests (#5726) This change is to improve code coverage (and to simplify #5720 and #5725); there is otherwise no change in functionality. The change adds basic tests for `STInteger` and `STParsedJSON`, so it becomes easier to test smaller changes to the types, as well as removes `STParsedJSONArray`, since it is not used anywhere (including in Clio). * Revert "Update Conan dependencies: OpenSSL" (#5807) This change reverts #5617, because it will require extensive testing that will take up more time than we have before the next scheduled release. Reverting this change does not mean we are abandoning it. We aim to pick it back up once there's a sufficient time window to allow for testing on multiple distros running a mixture of OpenSSL 1.x and 3.x. * docs: Add warning about using std::counting_semaphore (#5595) This adds a comment to avoid using `std::counting_semaphore` until the minimum compiler versions of GCC and Clang have been updated to no longer contain the bug that is present in older compilers. * Improve ValidatorList invalid UNL manifest logging (#5804) This change raises logging severity from `INFO` to `WARN` when handling UNL manifest signed with an unexpected / invalid key. It also changes the internal error code for an invalid format of UNL manifest to `invalid` (from `untrusted`). This is a follow up to problems experienced by an UNL node due to old manifest key configured in `validators.txt`, which would be easier to diagnose with improved logging. It also replaces a log line with `UNREACHABLE` for an impossible situation when we match UNL manifest key against a configured key which has an invalid type (we cannot configure such a key because of checks when loading configured keys). * chore: Pin all CI Docker tags (#5813) To avoid surprises and ensure reproducibility, this change pins all CI Docker image tags to the latest version in the XRPLF/CI repo. * change `fixPriceOracleOrder` to `Supported::yes` (#5749) * fix: Address http header case sensitivity (#5767) This change makes the regex in `HttpClient.cpp` that matches the content-length http header case insensitive to improve compatibility, as http headers are case insensitive. * test: add more comprehensive tests for `FeeVote` (#5746) This change adds more comprehensive tests for the `FeeVote` module, which previously only checked the basics, and not the more comprehensive flows in that class. * ci: Call all reusable workflows reusable (#5818) * Add `STInt32` as a new `SType` (#5788) This change adds `STInt32` as a new `SType` under the `STInteger` umbrella, with `SType` value `12`. This is the first and only `STInteger` type that supports negative values. * switch `fixIncludeKeyletFields` to `Supported::yes` (#5819) * refactor: Restructure Transactor::preflight to reduce boilerplate (#5592) * Restructures `Transactor::preflight` to create several functions that will remove the need for error-prone boilerplate code in derived classes' implementations of `preflight`. * refactor: Add support for extra transaction signatures (#5594) * Restructures Transactor signature checking code to be able to handle a `sigObject`, which may be the full transaction, or may be an object field containing a separate signature. Either way, the `sigObject` can be a single- or multi-sign signature. * ci: Upload artifacts during build and test in a separate job (#5817) * chore: Set free-form CI inputs as env vars (#5822) This change moves CI values that could be user-provided into environment variables. * Rename flags for DynamicMPT (#5820) * Set version to 2.6.1 * fix: FD/handle guarding + exponential backoff (#5823) * fix: Transaction sig checking functions do not get a full context (#5829) Fixes a (currently harmless) bug introduced by PR #5594 * Remove bogus coverage warning (#5838) * fix return type --------- Co-authored-by: Ed Hennis Co-authored-by: Jingchen Co-authored-by: Denis Angell Co-authored-by: Bart Co-authored-by: yinyiqian1 Co-authored-by: Vito Tumas <5780819+Tapanito@users.noreply.github.com> Co-authored-by: Bronek Kozicki Co-authored-by: Mayukha Vadari Co-authored-by: Valentin Balaschenko <13349202+vlntb@users.noreply.github.com> Co-authored-by: tequ Co-authored-by: Ayaz Salikhov --- .codecov.yml | 1 + .github/actions/build-deps/action.yml | 16 +- .github/actions/build-test/action.yml | 96 - .github/actions/print-env/action.yml | 43 + .github/actions/setup-conan/action.yml | 7 +- .github/scripts/levelization/README.md | 6 +- .../scripts/levelization/results/loops.txt | 3 - .../scripts/levelization/results/ordering.txt | 21 +- .github/scripts/strategy-matrix/generate.py | 2 +- .github/scripts/strategy-matrix/windows.json | 2 +- .github/workflows/build-test.yml | 146 - .github/workflows/on-pr.yml | 23 +- .github/workflows/on-trigger.yml | 31 +- .github/workflows/pre-commit.yml | 3 +- .github/workflows/publish-docs.yml | 2 +- .../workflows/reusable-build-test-config.yml | 69 + .github/workflows/reusable-build-test.yml | 58 + .github/workflows/reusable-build.yml | 121 + ...on.yml => reusable-check-levelization.yml} | 0 ...yml => reusable-check-missing-commits.yml} | 0 ...tify-clio.yml => reusable-notify-clio.yml} | 19 +- .../workflows/reusable-strategy-matrix.yml | 5 +- .github/workflows/reusable-test.yml | 69 + .github/workflows/upload-conan-deps.yml | 13 +- cmake/RippledCompiler.cmake | 17 +- cmake/RippledCore.cmake | 7 + cmake/RippledCov.cmake | 2 +- cmake/RippledInstall.cmake | 1 + conan.lock | 2 +- conanfile.py | 2 +- include/xrpl/basics/base_uint.h | 10 + include/xrpl/basics/safe_cast.h | 7 +- include/xrpl/beast/utility/instrumentation.h | 5 + .../xrpld => include/xrpl}/ledger/ApplyView.h | 5 +- .../xrpl}/ledger/ApplyViewImpl.h | 5 +- {src/xrpld => include/xrpl}/ledger/BookDirs.h | 3 +- .../xrpl}/ledger/CachedSLEs.h | 0 .../xrpl}/ledger/CachedView.h | 5 +- .../xrpl/ledger}/CredentialHelpers.h | 5 +- {src/xrpld => include/xrpl}/ledger/Dir.h | 3 +- {src/xrpld => include/xrpl}/ledger/OpenView.h | 7 +- .../xrpl}/ledger/PaymentSandbox.h | 7 +- {src/xrpld => include/xrpl}/ledger/RawView.h | 3 +- {src/xrpld => include/xrpl}/ledger/ReadView.h | 5 +- {src/xrpld => include/xrpl}/ledger/Sandbox.h | 4 +- {src/xrpld => include/xrpl}/ledger/View.h | 26 +- .../xrpl}/ledger/detail/ApplyStateTable.h | 7 +- .../xrpl}/ledger/detail/ApplyViewBase.h | 7 +- .../xrpl}/ledger/detail/RawStateTable.h | 4 +- .../xrpl}/ledger/detail/ReadViewFwdRange.h | 0 .../xrpl}/ledger/detail/ReadViewFwdRange.ipp | 0 include/xrpl/protocol/FeeUnits.h | 565 ---- include/xrpl/protocol/Indexes.h | 2 + include/xrpl/protocol/LedgerFormats.h | 11 +- include/xrpl/protocol/Permissions.h | 6 + include/xrpl/protocol/Protocol.h | 1 - include/xrpl/protocol/SField.h | 21 +- include/xrpl/protocol/STInteger.h | 2 + include/xrpl/protocol/STLedgerEntry.h | 9 +- include/xrpl/protocol/STObject.h | 6 +- include/xrpl/protocol/STParsedJSON.h | 28 - include/xrpl/protocol/STValidation.h | 2 +- include/xrpl/protocol/TER.h | 3 +- include/xrpl/protocol/TxFlags.h | 38 +- include/xrpl/protocol/Units.h | 555 ++++ include/xrpl/protocol/XRPAmount.h | 4 +- include/xrpl/protocol/detail/features.macro | 7 +- .../xrpl/protocol/detail/ledger_entries.macro | 7 +- include/xrpl/protocol/detail/sfields.macro | 15 +- .../xrpl/protocol/detail/transactions.macro | 241 +- include/xrpl/protocol/jss.h | 6 +- include/xrpl/server/detail/Door.h | 119 +- .../ledger}/ApplyStateTable.cpp | 3 +- .../detail => libxrpl/ledger}/ApplyView.cpp | 3 +- .../ledger}/ApplyViewBase.cpp | 2 +- .../ledger}/ApplyViewImpl.cpp | 2 +- .../detail => libxrpl/ledger}/BookDirs.cpp | 5 +- .../detail => libxrpl/ledger}/CachedView.cpp | 3 +- .../ledger}/CredentialHelpers.cpp | 5 +- .../ledger/detail => libxrpl/ledger}/Dir.cpp | 2 +- .../detail => libxrpl/ledger}/OpenView.cpp | 3 +- .../ledger}/PaymentSandbox.cpp | 6 +- .../ledger}/RawStateTable.cpp | 3 +- .../detail => libxrpl/ledger}/ReadView.cpp | 2 +- .../ledger/detail => libxrpl/ledger}/View.cpp | 160 +- src/libxrpl/net/HTTPClient.cpp | 2 +- src/libxrpl/protocol/BuildInfo.cpp | 2 +- src/libxrpl/protocol/Permissions.cpp | 37 +- src/libxrpl/protocol/SField.cpp | 35 +- src/libxrpl/protocol/STInteger.cpp | 61 +- src/libxrpl/protocol/STObject.cpp | 12 + src/libxrpl/protocol/STParsedJSON.cpp | 451 ++-- src/libxrpl/protocol/STVar.cpp | 3 + src/libxrpl/protocol/Serializer.cpp | 6 + src/libxrpl/protocol/TxFormats.cpp | 3 +- src/test/app/AMMCalc_test.cpp | 2 +- src/test/app/AMMExtended_test.cpp | 196 +- src/test/app/AMM_test.cpp | 205 +- src/test/app/Credentials_test.cpp | 14 +- src/test/app/DID_test.cpp | 8 - src/test/app/EscrowToken_test.cpp | 78 +- src/test/app/Escrow_test.cpp | 11 +- src/test/app/FeeVote_test.cpp | 699 +++++ src/test/app/Flow_test.cpp | 4 +- src/test/app/Invariants_test.cpp | 132 +- src/test/app/LPTokenTransfer_test.cpp | 16 +- src/test/app/LedgerHistory_test.cpp | 2 +- src/test/app/LoadFeeTrack_test.cpp | 2 +- src/test/app/MPToken_test.cpp | 929 ++++++- src/test/app/MultiSign_test.cpp | 115 +- src/test/app/NetworkOPs_test.cpp | 80 + src/test/app/Oracle_test.cpp | 23 +- src/test/app/PayChan_test.cpp | 11 +- src/test/app/PayStrand_test.cpp | 2 +- src/test/app/PermissionedDEX_test.cpp | 2 +- src/test/app/TheoreticalQuality_test.cpp | 2 +- src/test/app/ValidatorList_test.cpp | 18 + src/test/app/ValidatorSite_test.cpp | 2 +- src/test/app/Vault_test.cpp | 45 +- src/test/basics/FileUtilities_test.cpp | 2 +- .../{FeeUnits_test.cpp => Units_test.cpp} | 32 +- src/test/basics/base_uint_test.cpp | 5 + src/test/consensus/NegativeUNL_test.cpp | 2 +- src/test/core/Config_test.cpp | 6 +- src/test/csf/Digraph.h | 6 +- src/test/jtx/Env.h | 3 + src/test/jtx/TestHelpers.h | 277 +- src/test/jtx/amount.h | 84 +- src/test/jtx/balance.h | 8 +- src/test/jtx/escrow.h | 81 +- src/test/jtx/fee.h | 5 + src/test/jtx/flags.h | 32 +- src/test/jtx/impl/Env.cpp | 12 +- src/test/jtx/impl/Oracle.cpp | 4 +- src/test/jtx/impl/TestHelpers.cpp | 30 +- src/test/jtx/impl/amount.cpp | 8 +- src/test/jtx/impl/balance.cpp | 55 +- src/test/jtx/impl/fee.cpp | 9 +- src/test/jtx/impl/mpt.cpp | 93 +- src/test/jtx/impl/owners.cpp | 8 +- src/test/jtx/mpt.h | 16 + src/test/jtx/owners.h | 10 +- src/test/jtx/require.h | 7 +- src/test/jtx/tags.h | 10 + src/test/ledger/BookDirs_test.cpp | 3 +- src/test/ledger/Directory_test.cpp | 5 +- src/test/ledger/PaymentSandbox_test.cpp | 7 +- src/test/ledger/SkipList_test.cpp | 2 +- src/test/ledger/View_test.cpp | 8 +- src/test/nodestore/import_test.cpp | 3 +- src/test/protocol/STAccount_test.cpp | 17 + src/test/protocol/STInteger_test.cpp | 163 ++ src/test/protocol/STObject_test.cpp | 215 -- src/test/protocol/STParsedJSON_test.cpp | 2356 +++++++++++++++++ src/test/rpc/AccountSet_test.cpp | 29 + src/test/unit_test/FileDirGuard.h | 2 - src/test/unit_test/multi_runner.cpp | 6 +- src/test/unit_test/multi_runner.h | 3 +- src/tests/libxrpl/CMakeLists.txt | 2 + src/tests/libxrpl/net/HTTPClient.cpp | 346 +++ .../libxrpl/net/main.cpp} | 37 +- src/xrpld/app/consensus/RCLCxLedger.h | 2 +- src/xrpld/app/ledger/BuildLedger.h | 3 +- src/xrpld/app/ledger/Ledger.h | 4 +- src/xrpld/app/ledger/LocalTxs.h | 3 +- src/xrpld/app/ledger/OpenLedger.h | 4 +- src/xrpld/app/ledger/detail/OpenLedger.cpp | 2 +- src/xrpld/app/main/Application.cpp | 4 +- src/xrpld/app/misc/AMMUtils.h | 3 +- src/xrpld/app/misc/FeeVote.h | 2 +- src/xrpld/app/misc/NetworkOPs.cpp | 5 + src/xrpld/app/misc/NetworkOPs.h | 2 +- src/xrpld/app/misc/PermissionedDEXHelpers.cpp | 3 +- src/xrpld/app/misc/PermissionedDEXHelpers.h | 2 +- src/xrpld/app/misc/TxQ.h | 4 +- src/xrpld/app/misc/ValidatorList.h | 2 +- src/xrpld/app/misc/detail/AMMUtils.cpp | 2 +- src/xrpld/app/misc/detail/LoadFeeTrack.cpp | 3 +- src/xrpld/app/misc/detail/ValidatorList.cpp | 40 +- src/xrpld/app/paths/AMMLiquidity.h | 4 +- src/xrpld/app/paths/AMMOffer.h | 5 +- src/xrpld/app/paths/Credit.cpp | 3 +- src/xrpld/app/paths/Credit.h | 3 +- src/xrpld/app/paths/Pathfinder.cpp | 2 +- src/xrpld/app/paths/RippleCalc.cpp | 2 +- src/xrpld/app/paths/RippleCalc.h | 3 +- src/xrpld/app/paths/TrustLine.h | 3 +- src/xrpld/app/paths/detail/BookStep.cpp | 2 +- src/xrpld/app/paths/detail/DirectStep.cpp | 2 +- src/xrpld/app/paths/detail/FlowDebugInfo.h | 2 +- src/xrpld/app/paths/detail/PaySteps.cpp | 2 +- src/xrpld/app/paths/detail/StepChecks.h | 5 +- .../app/paths/detail/XRPEndpointStep.cpp | 2 +- src/xrpld/app/tx/apply.h | 2 +- src/xrpld/app/tx/applySteps.h | 3 +- src/xrpld/app/tx/detail/AMMBid.cpp | 24 +- src/xrpld/app/tx/detail/AMMBid.h | 3 + src/xrpld/app/tx/detail/AMMClawback.cpp | 24 +- src/xrpld/app/tx/detail/AMMClawback.h | 3 + src/xrpld/app/tx/detail/AMMCreate.cpp | 26 +- src/xrpld/app/tx/detail/AMMCreate.h | 3 + src/xrpld/app/tx/detail/AMMDelete.cpp | 22 +- src/xrpld/app/tx/detail/AMMDelete.h | 3 + src/xrpld/app/tx/detail/AMMDeposit.cpp | 30 +- src/xrpld/app/tx/detail/AMMDeposit.h | 6 + src/xrpld/app/tx/detail/AMMVote.cpp | 22 +- src/xrpld/app/tx/detail/AMMVote.h | 3 + src/xrpld/app/tx/detail/AMMWithdraw.cpp | 27 +- src/xrpld/app/tx/detail/AMMWithdraw.h | 9 +- src/xrpld/app/tx/detail/ApplyContext.h | 2 +- src/xrpld/app/tx/detail/Batch.cpp | 47 +- src/xrpld/app/tx/detail/Batch.h | 6 + src/xrpld/app/tx/detail/BookTip.h | 3 +- src/xrpld/app/tx/detail/CancelCheck.cpp | 18 +- src/xrpld/app/tx/detail/CancelOffer.cpp | 16 +- src/xrpld/app/tx/detail/CashCheck.cpp | 16 +- src/xrpld/app/tx/detail/Change.cpp | 9 +- src/xrpld/app/tx/detail/Change.h | 3 - src/xrpld/app/tx/detail/Clawback.cpp | 19 +- src/xrpld/app/tx/detail/Clawback.h | 3 + src/xrpld/app/tx/detail/CreateCheck.cpp | 17 +- src/xrpld/app/tx/detail/CreateOffer.cpp | 38 +- src/xrpld/app/tx/detail/CreateOffer.h | 6 + src/xrpld/app/tx/detail/CreateTicket.cpp | 11 +- src/xrpld/app/tx/detail/Credentials.cpp | 82 +- src/xrpld/app/tx/detail/Credentials.h | 9 + src/xrpld/app/tx/detail/DID.cpp | 26 +- src/xrpld/app/tx/detail/DelegateSet.cpp | 10 +- src/xrpld/app/tx/detail/DeleteAccount.cpp | 36 +- src/xrpld/app/tx/detail/DeleteAccount.h | 3 + src/xrpld/app/tx/detail/DeleteOracle.cpp | 16 +- src/xrpld/app/tx/detail/DepositPreauth.cpp | 37 +- src/xrpld/app/tx/detail/DepositPreauth.h | 3 + src/xrpld/app/tx/detail/Escrow.cpp | 93 +- src/xrpld/app/tx/detail/Escrow.h | 15 + src/xrpld/app/tx/detail/InvariantCheck.cpp | 334 ++- src/xrpld/app/tx/detail/InvariantCheck.h | 31 +- src/xrpld/app/tx/detail/LedgerStateFix.cpp | 15 +- src/xrpld/app/tx/detail/MPTokenAuthorize.cpp | 19 +- src/xrpld/app/tx/detail/MPTokenAuthorize.h | 3 + .../app/tx/detail/MPTokenIssuanceCreate.cpp | 42 +- .../app/tx/detail/MPTokenIssuanceCreate.h | 7 + .../app/tx/detail/MPTokenIssuanceDestroy.cpp | 20 +- .../app/tx/detail/MPTokenIssuanceDestroy.h | 3 + .../app/tx/detail/MPTokenIssuanceSet.cpp | 182 +- src/xrpld/app/tx/detail/MPTokenIssuanceSet.h | 6 + .../app/tx/detail/NFTokenAcceptOffer.cpp | 19 +- src/xrpld/app/tx/detail/NFTokenAcceptOffer.h | 3 + src/xrpld/app/tx/detail/NFTokenBurn.cpp | 11 +- .../app/tx/detail/NFTokenCancelOffer.cpp | 19 +- src/xrpld/app/tx/detail/NFTokenCancelOffer.h | 3 + .../app/tx/detail/NFTokenCreateOffer.cpp | 19 +- src/xrpld/app/tx/detail/NFTokenCreateOffer.h | 3 + src/xrpld/app/tx/detail/NFTokenMint.cpp | 38 +- src/xrpld/app/tx/detail/NFTokenMint.h | 6 + src/xrpld/app/tx/detail/NFTokenModify.cpp | 18 +- src/xrpld/app/tx/detail/NFTokenModify.h | 3 + src/xrpld/app/tx/detail/NFTokenUtils.cpp | 4 +- src/xrpld/app/tx/detail/NFTokenUtils.h | 2 +- src/xrpld/app/tx/detail/Offer.h | 3 +- src/xrpld/app/tx/detail/OfferStream.cpp | 2 +- src/xrpld/app/tx/detail/OfferStream.h | 2 +- src/xrpld/app/tx/detail/PayChan.cpp | 66 +- src/xrpld/app/tx/detail/PayChan.h | 12 + src/xrpld/app/tx/detail/Payment.cpp | 41 +- src/xrpld/app/tx/detail/Payment.h | 6 + .../tx/detail/PermissionedDomainDelete.cpp | 16 +- .../app/tx/detail/PermissionedDomainSet.cpp | 25 +- .../app/tx/detail/PermissionedDomainSet.h | 3 + src/xrpld/app/tx/detail/SetAccount.cpp | 19 +- src/xrpld/app/tx/detail/SetAccount.h | 3 + src/xrpld/app/tx/detail/SetOracle.cpp | 24 +- src/xrpld/app/tx/detail/SetRegularKey.cpp | 14 +- src/xrpld/app/tx/detail/SetSignerList.cpp | 29 +- src/xrpld/app/tx/detail/SetSignerList.h | 3 + src/xrpld/app/tx/detail/SetTrust.cpp | 19 +- src/xrpld/app/tx/detail/SetTrust.h | 3 + src/xrpld/app/tx/detail/Taker.h | 0 src/xrpld/app/tx/detail/Transactor.cpp | 249 +- src/xrpld/app/tx/detail/Transactor.h | 184 +- src/xrpld/app/tx/detail/VaultClawback.cpp | 13 +- src/xrpld/app/tx/detail/VaultCreate.cpp | 71 +- src/xrpld/app/tx/detail/VaultCreate.h | 6 + src/xrpld/app/tx/detail/VaultDelete.cpp | 13 +- src/xrpld/app/tx/detail/VaultDeposit.cpp | 15 +- src/xrpld/app/tx/detail/VaultSet.cpp | 24 +- src/xrpld/app/tx/detail/VaultSet.h | 3 + src/xrpld/app/tx/detail/VaultWithdraw.cpp | 15 +- src/xrpld/app/tx/detail/XChainBridge.cpp | 95 +- src/xrpld/app/tx/detail/XChainBridge.h | 3 + src/xrpld/app/tx/detail/applySteps.cpp | 67 +- src/xrpld/core/detail/semaphore.h | 28 + src/xrpld/overlay/detail/ConnectAttempt.cpp | 488 +++- src/xrpld/overlay/detail/ConnectAttempt.h | 208 +- src/xrpld/overlay/detail/PeerImp.cpp | 404 +-- src/xrpld/overlay/detail/PeerImp.h | 208 +- src/xrpld/rpc/detail/RPCHelpers.cpp | 4 +- src/xrpld/rpc/handlers/AMMInfo.cpp | 2 +- src/xrpld/rpc/handlers/AccountChannels.cpp | 4 +- .../rpc/handlers/AccountCurrenciesHandler.cpp | 2 +- src/xrpld/rpc/handlers/AccountInfo.cpp | 2 +- src/xrpld/rpc/handlers/AccountLines.cpp | 2 +- src/xrpld/rpc/handlers/AccountObjects.cpp | 2 +- src/xrpld/rpc/handlers/AccountOffers.cpp | 4 +- src/xrpld/rpc/handlers/AccountTx.cpp | 2 +- src/xrpld/rpc/handlers/BookOffers.cpp | 2 +- src/xrpld/rpc/handlers/DepositAuthorized.cpp | 4 +- src/xrpld/rpc/handlers/GatewayBalances.cpp | 2 +- src/xrpld/rpc/handlers/GetAggregatePrice.cpp | 2 +- src/xrpld/rpc/handlers/LedgerData.cpp | 2 +- src/xrpld/rpc/handlers/LedgerEntry.cpp | 4 +- src/xrpld/rpc/handlers/LedgerHandler.h | 2 +- src/xrpld/rpc/handlers/LedgerHeader.cpp | 2 +- src/xrpld/rpc/handlers/NFTOffers.cpp | 4 +- src/xrpld/rpc/handlers/NoRippleCheck.cpp | 2 +- src/xrpld/rpc/handlers/PayChanClaim.cpp | 2 +- src/xrpld/rpc/handlers/Subscribe.cpp | 2 +- src/xrpld/rpc/handlers/TransactionEntry.cpp | 2 +- 318 files changed, 10509 insertions(+), 3596 deletions(-) delete mode 100644 .github/actions/build-test/action.yml create mode 100644 .github/actions/print-env/action.yml delete mode 100644 .github/workflows/build-test.yml create mode 100644 .github/workflows/reusable-build-test-config.yml create mode 100644 .github/workflows/reusable-build-test.yml create mode 100644 .github/workflows/reusable-build.yml rename .github/workflows/{check-levelization.yml => reusable-check-levelization.yml} (100%) rename .github/workflows/{check-missing-commits.yml => reusable-check-missing-commits.yml} (100%) rename .github/workflows/{notify-clio.yml => reusable-notify-clio.yml} (83%) create mode 100644 .github/workflows/reusable-test.yml rename {src/xrpld => include/xrpl}/ledger/ApplyView.h (99%) rename {src/xrpld => include/xrpl}/ledger/ApplyViewImpl.h (97%) rename {src/xrpld => include/xrpl}/ledger/BookDirs.h (98%) rename {src/xrpld => include/xrpl}/ledger/CachedSLEs.h (100%) rename {src/xrpld => include/xrpl}/ledger/CachedView.h (98%) rename {src/xrpld/app/misc => include/xrpl/ledger}/CredentialHelpers.h (98%) rename {src/xrpld => include/xrpl}/ledger/Dir.h (98%) rename {src/xrpld => include/xrpl}/ledger/OpenView.h (98%) rename {src/xrpld => include/xrpl}/ledger/PaymentSandbox.h (98%) rename {src/xrpld => include/xrpl}/ledger/RawView.h (98%) rename {src/xrpld => include/xrpl}/ledger/ReadView.h (98%) rename {src/xrpld => include/xrpl}/ledger/Sandbox.h (95%) rename {src/xrpld => include/xrpl}/ledger/View.h (96%) rename {src/xrpld => include/xrpl}/ledger/detail/ApplyStateTable.h (97%) rename {src/xrpld => include/xrpl}/ledger/detail/ApplyViewBase.h (96%) rename {src/xrpld => include/xrpl}/ledger/detail/RawStateTable.h (98%) rename {src/xrpld => include/xrpl}/ledger/detail/ReadViewFwdRange.h (100%) rename {src/xrpld => include/xrpl}/ledger/detail/ReadViewFwdRange.ipp (100%) delete mode 100644 include/xrpl/protocol/FeeUnits.h create mode 100644 include/xrpl/protocol/Units.h rename src/{xrpld/ledger/detail => libxrpl/ledger}/ApplyStateTable.cpp (99%) rename src/{xrpld/ledger/detail => libxrpl/ledger}/ApplyView.cpp (99%) rename src/{xrpld/ledger/detail => libxrpl/ledger}/ApplyViewBase.cpp (98%) rename src/{xrpld/ledger/detail => libxrpl/ledger}/ApplyViewImpl.cpp (97%) rename src/{xrpld/ledger/detail => libxrpl/ledger}/BookDirs.cpp (98%) rename src/{xrpld/ledger/detail => libxrpl/ledger}/CachedView.cpp (98%) rename src/{xrpld/app/misc => libxrpl/ledger}/CredentialHelpers.cpp (99%) rename src/{xrpld/ledger/detail => libxrpl/ledger}/Dir.cpp (99%) rename src/{xrpld/ledger/detail => libxrpl/ledger}/OpenView.cpp (99%) rename src/{xrpld/ledger/detail => libxrpl/ledger}/PaymentSandbox.cpp (99%) rename src/{xrpld/ledger/detail => libxrpl/ledger}/RawStateTable.cpp (99%) rename src/{xrpld/ledger/detail => libxrpl/ledger}/ReadView.cpp (98%) rename src/{xrpld/ledger/detail => libxrpl/ledger}/View.cpp (95%) create mode 100644 src/test/app/NetworkOPs_test.cpp rename src/test/basics/{FeeUnits_test.cpp => Units_test.cpp} (92%) create mode 100644 src/test/protocol/STInteger_test.cpp create mode 100644 src/test/protocol/STParsedJSON_test.cpp create mode 100644 src/tests/libxrpl/net/HTTPClient.cpp rename src/{test/protocol/types_test.cpp => tests/libxrpl/net/main.cpp} (56%) create mode 100644 src/xrpld/app/tx/detail/Taker.h diff --git a/.codecov.yml b/.codecov.yml index d28d7c80df..cd52e2604d 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -33,5 +33,6 @@ slack_app: false ignore: - "src/test/" + - "src/tests/" - "include/xrpl/beast/test/" - "include/xrpl/beast/unit_test/" diff --git a/.github/actions/build-deps/action.yml b/.github/actions/build-deps/action.yml index 351d8a6361..a908c656e8 100644 --- a/.github/actions/build-deps/action.yml +++ b/.github/actions/build-deps/action.yml @@ -20,14 +20,18 @@ runs: steps: - name: Install Conan dependencies shell: bash + env: + BUILD_DIR: ${{ inputs.build_dir }} + BUILD_OPTION: ${{ inputs.force_build == 'true' && '*' || 'missing' }} + BUILD_TYPE: ${{ inputs.build_type }} run: | echo 'Installing dependencies.' - mkdir -p ${{ inputs.build_dir }} - cd ${{ inputs.build_dir }} + mkdir -p '${{ env.BUILD_DIR }}' + cd '${{ env.BUILD_DIR }}' conan install \ --output-folder . \ - --build ${{ inputs.force_build == 'true' && '"*"' || 'missing' }} \ - --options:host '&:tests=True' \ - --options:host '&:xrpld=True' \ - --settings:all build_type=${{ inputs.build_type }} \ + --build=${{ env.BUILD_OPTION }} \ + --options:host='&:tests=True' \ + --options:host='&:xrpld=True' \ + --settings:all build_type='${{ env.BUILD_TYPE }}' \ .. diff --git a/.github/actions/build-test/action.yml b/.github/actions/build-test/action.yml deleted file mode 100644 index cf1bac16f7..0000000000 --- a/.github/actions/build-test/action.yml +++ /dev/null @@ -1,96 +0,0 @@ -# This action build and tests the binary. The Conan dependencies must have -# already been installed (see the build-deps action). -name: Build and Test -description: "Build and test the binary." - -# Note that actions do not support 'type' and all inputs are strings, see -# https://docs.github.com/en/actions/reference/workflows-and-actions/metadata-syntax#inputs. -inputs: - build_dir: - description: "The directory where to build." - required: true - build_only: - description: 'Whether to only build or to build and test the code ("true", "false").' - required: false - default: "false" - build_type: - description: 'The build type to use ("Debug", "Release").' - required: true - cmake_args: - description: "Additional arguments to pass to CMake." - required: false - default: "" - cmake_target: - description: "The CMake target to build." - required: true - codecov_token: - description: "The Codecov token to use for uploading coverage reports." - required: false - default: "" - os: - description: 'The operating system to use for the build ("linux", "macos", "windows").' - required: true - -runs: - using: composite - steps: - - name: Configure CMake - shell: bash - working-directory: ${{ inputs.build_dir }} - run: | - echo 'Configuring CMake.' - cmake \ - -G '${{ inputs.os == 'windows' && 'Visual Studio 17 2022' || 'Ninja' }}' \ - -DCMAKE_TOOLCHAIN_FILE:FILEPATH=build/generators/conan_toolchain.cmake \ - -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} \ - ${{ inputs.cmake_args }} \ - .. - - name: Build the binary - shell: bash - working-directory: ${{ inputs.build_dir }} - run: | - echo 'Building binary.' - cmake \ - --build . \ - --config ${{ inputs.build_type }} \ - --parallel $(nproc) \ - --target ${{ inputs.cmake_target }} - - name: Check linking - if: ${{ inputs.os == 'linux' }} - shell: bash - working-directory: ${{ inputs.build_dir }} - run: | - echo 'Checking linking.' - ldd ./rippled - if [ "$(ldd ./rippled | grep -E '(libstdc\+\+|libgcc)' | wc -l)" -eq 0 ]; then - echo 'The binary is statically linked.' - else - echo 'The binary is dynamically linked.' - exit 1 - fi - - name: Verify voidstar - if: ${{ contains(inputs.cmake_args, '-Dvoidstar=ON') }} - shell: bash - working-directory: ${{ inputs.build_dir }} - run: | - echo 'Verifying presence of instrumentation.' - ./rippled --version | grep libvoidstar - - name: Test the binary - if: ${{ inputs.build_only == 'false' }} - shell: bash - working-directory: ${{ inputs.build_dir }}/${{ inputs.os == 'windows' && inputs.build_type || '' }} - run: | - echo 'Testing binary.' - ./rippled --unittest --unittest-jobs $(nproc) - ctest -j $(nproc) --output-on-failure - - name: Upload coverage report - if: ${{ inputs.cmake_target == 'coverage' }} - uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 - with: - disable_search: true - disable_telem: true - fail_ci_if_error: true - files: ${{ inputs.build_dir }}/coverage.xml - plugins: noop - token: ${{ inputs.codecov_token }} - verbose: true diff --git a/.github/actions/print-env/action.yml b/.github/actions/print-env/action.yml new file mode 100644 index 0000000000..6019a6de2f --- /dev/null +++ b/.github/actions/print-env/action.yml @@ -0,0 +1,43 @@ +name: Print build environment +description: "Print environment and some tooling versions" + +runs: + using: composite + steps: + - name: Check configuration (Windows) + if: ${{ runner.os == 'Windows' }} + shell: bash + run: | + echo 'Checking environment variables.' + set + + echo 'Checking CMake version.' + cmake --version + + echo 'Checking Conan version.' + conan --version + + - name: Check configuration (Linux and macOS) + if: ${{ runner.os == 'Linux' || runner.os == 'macOS' }} + shell: bash + run: | + echo 'Checking path.' + echo ${PATH} | tr ':' '\n' + + echo 'Checking environment variables.' + env | sort + + echo 'Checking CMake version.' + cmake --version + + echo 'Checking compiler version.' + ${{ runner.os == 'Linux' && '${CC}' || 'clang' }} --version + + echo 'Checking Conan version.' + conan --version + + echo 'Checking Ninja version.' + ninja --version + + echo 'Checking nproc version.' + nproc --version diff --git a/.github/actions/setup-conan/action.yml b/.github/actions/setup-conan/action.yml index d31809dc94..02061c7a64 100644 --- a/.github/actions/setup-conan/action.yml +++ b/.github/actions/setup-conan/action.yml @@ -35,9 +35,12 @@ runs: - name: Set up Conan remote shell: bash + env: + CONAN_REMOTE_NAME: ${{ inputs.conan_remote_name }} + CONAN_REMOTE_URL: ${{ inputs.conan_remote_url }} run: | - echo "Adding Conan remote '${{ inputs.conan_remote_name }}' at ${{ inputs.conan_remote_url }}." - conan remote add --index 0 --force ${{ inputs.conan_remote_name }} ${{ inputs.conan_remote_url }} + echo "Adding Conan remote '${{ env.CONAN_REMOTE_NAME }}' at '${{ env.CONAN_REMOTE_URL }}'." + conan remote add --index 0 --force '${{ env.CONAN_REMOTE_NAME }}' '${{ env.CONAN_REMOTE_URL }}' echo 'Listing Conan remotes.' conan remote list diff --git a/.github/scripts/levelization/README.md b/.github/scripts/levelization/README.md index 31c6d34b6b..f3ba1e2518 100644 --- a/.github/scripts/levelization/README.md +++ b/.github/scripts/levelization/README.md @@ -72,15 +72,15 @@ It generates many files of [results](results): desired as described above. In a perfect repo, this file will be empty. This file is committed to the repo, and is used by the [levelization - Github workflow](../../workflows/check-levelization.yml) to validate + Github workflow](../../workflows/reusable-check-levelization.yml) to validate that nothing changed. - [`ordering.txt`](results/ordering.txt): A list showing relationships between modules where there are no loops as they actually exist, as opposed to how they are desired as described above. This file is committed to the repo, and is used by the [levelization - Github workflow](../../workflows/check-levelization.yml) to validate + Github workflow](../../workflows/reusable-check-levelization.yml) to validate that nothing changed. -- [`levelization.yml`](../../workflows/check-levelization.yml) +- [`levelization.yml`](../../workflows/reusable-check-levelization.yml) Github Actions workflow to test that levelization loops haven't changed. Unfortunately, if changes are detected, it can't tell if they are improvements or not, so if you have resolved any issues or diff --git a/.github/scripts/levelization/results/loops.txt b/.github/scripts/levelization/results/loops.txt index 0bbd65a9e4..e998e962d4 100644 --- a/.github/scripts/levelization/results/loops.txt +++ b/.github/scripts/levelization/results/loops.txt @@ -7,9 +7,6 @@ Loop: test.jtx test.unit_test Loop: xrpld.app xrpld.core xrpld.app > xrpld.core -Loop: xrpld.app xrpld.ledger - xrpld.app > xrpld.ledger - Loop: xrpld.app xrpld.overlay xrpld.overlay > xrpld.app diff --git a/.github/scripts/levelization/results/ordering.txt b/.github/scripts/levelization/results/ordering.txt index bf2d1db693..55df4c2672 100644 --- a/.github/scripts/levelization/results/ordering.txt +++ b/.github/scripts/levelization/results/ordering.txt @@ -2,6 +2,10 @@ libxrpl.basics > xrpl.basics libxrpl.crypto > xrpl.basics libxrpl.json > xrpl.basics libxrpl.json > xrpl.json +libxrpl.ledger > xrpl.basics +libxrpl.ledger > xrpl.json +libxrpl.ledger > xrpl.ledger +libxrpl.ledger > xrpl.protocol libxrpl.net > xrpl.basics libxrpl.net > xrpl.net libxrpl.protocol > xrpl.basics @@ -21,11 +25,11 @@ test.app > test.unit_test test.app > xrpl.basics test.app > xrpld.app test.app > xrpld.core -test.app > xrpld.ledger test.app > xrpld.nodestore test.app > xrpld.overlay test.app > xrpld.rpc test.app > xrpl.json +test.app > xrpl.ledger test.app > xrpl.protocol test.app > xrpl.resource test.basics > test.jtx @@ -44,8 +48,8 @@ test.consensus > test.unit_test test.consensus > xrpl.basics test.consensus > xrpld.app test.consensus > xrpld.consensus -test.consensus > xrpld.ledger test.consensus > xrpl.json +test.consensus > xrpl.ledger test.core > test.jtx test.core > test.toplevel test.core > test.unit_test @@ -63,9 +67,9 @@ test.json > xrpl.json test.jtx > xrpl.basics test.jtx > xrpld.app test.jtx > xrpld.core -test.jtx > xrpld.ledger test.jtx > xrpld.rpc test.jtx > xrpl.json +test.jtx > xrpl.ledger test.jtx > xrpl.net test.jtx > xrpl.protocol test.jtx > xrpl.resource @@ -75,7 +79,7 @@ test.ledger > test.toplevel test.ledger > xrpl.basics test.ledger > xrpld.app test.ledger > xrpld.core -test.ledger > xrpld.ledger +test.ledger > xrpl.ledger test.ledger > xrpl.protocol test.nodestore > test.jtx test.nodestore > test.toplevel @@ -134,7 +138,10 @@ test.toplevel > test.csf test.toplevel > xrpl.json test.unit_test > xrpl.basics tests.libxrpl > xrpl.basics +tests.libxrpl > xrpl.net xrpl.json > xrpl.basics +xrpl.ledger > xrpl.basics +xrpl.ledger > xrpl.protocol xrpl.net > xrpl.basics xrpl.protocol > xrpl.basics xrpl.protocol > xrpl.json @@ -151,6 +158,7 @@ xrpld.app > xrpld.consensus xrpld.app > xrpld.nodestore xrpld.app > xrpld.perflog xrpld.app > xrpl.json +xrpld.app > xrpl.ledger xrpld.app > xrpl.net xrpld.app > xrpl.protocol xrpld.app > xrpl.resource @@ -163,9 +171,6 @@ xrpld.core > xrpl.basics xrpld.core > xrpl.json xrpld.core > xrpl.net xrpld.core > xrpl.protocol -xrpld.ledger > xrpl.basics -xrpld.ledger > xrpl.json -xrpld.ledger > xrpl.protocol xrpld.nodestore > xrpl.basics xrpld.nodestore > xrpld.core xrpld.nodestore > xrpld.unity @@ -186,9 +191,9 @@ xrpld.perflog > xrpl.basics xrpld.perflog > xrpl.json xrpld.rpc > xrpl.basics xrpld.rpc > xrpld.core -xrpld.rpc > xrpld.ledger xrpld.rpc > xrpld.nodestore xrpld.rpc > xrpl.json +xrpld.rpc > xrpl.ledger xrpld.rpc > xrpl.net xrpld.rpc > xrpl.protocol xrpld.rpc > xrpl.resource diff --git a/.github/scripts/strategy-matrix/generate.py b/.github/scripts/strategy-matrix/generate.py index ac39803fff..fd05895b0e 100755 --- a/.github/scripts/strategy-matrix/generate.py +++ b/.github/scripts/strategy-matrix/generate.py @@ -162,7 +162,7 @@ def generate_strategy_matrix(all: bool, config: Config) -> list: 'config_name': config_name, 'cmake_args': cmake_args, 'cmake_target': cmake_target, - 'build_only': 'true' if build_only else 'false', + 'build_only': build_only, 'build_type': build_type, 'os': os, 'architecture': architecture, diff --git a/.github/scripts/strategy-matrix/windows.json b/.github/scripts/strategy-matrix/windows.json index 5e6e536750..08b41e3f89 100644 --- a/.github/scripts/strategy-matrix/windows.json +++ b/.github/scripts/strategy-matrix/windows.json @@ -2,7 +2,7 @@ "architecture": [ { "platform": "windows/amd64", - "runner": ["windows-latest"] + "runner": ["self-hosted", "Windows", "devbox"] } ], "os": [ diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml deleted file mode 100644 index 69ff986f98..0000000000 --- a/.github/workflows/build-test.yml +++ /dev/null @@ -1,146 +0,0 @@ -# This workflow builds and tests the binary for various configurations. -name: Build and test - -# This workflow can only be triggered by other workflows. Note that the -# workflow_call event does not support the 'choice' input type, see -# https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#onworkflow_callinputsinput_idtype, -# so we use 'string' instead. -on: - workflow_call: - inputs: - build_dir: - description: "The directory where to build." - required: false - type: string - default: ".build" - dependencies_force_build: - description: "Force building of all dependencies." - required: false - type: boolean - default: false - dependencies_force_upload: - description: "Force uploading of all dependencies." - required: false - type: boolean - default: false - os: - description: 'The operating system to use for the build ("linux", "macos", "windows").' - required: true - type: string - strategy_matrix: - # TODO: Support additional strategies, e.g. "ubuntu" for generating all Ubuntu configurations. - description: 'The strategy matrix to use for generating the configurations ("minimal", "all").' - required: false - type: string - default: "minimal" - secrets: - codecov_token: - description: "The Codecov token to use for uploading coverage reports." - required: false - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.os }} - cancel-in-progress: true - -defaults: - run: - shell: bash - -jobs: - # Generate the strategy matrix to be used by the following job. - generate-matrix: - uses: ./.github/workflows/reusable-strategy-matrix.yml - with: - os: ${{ inputs.os }} - strategy_matrix: ${{ inputs.strategy_matrix }} - - # Build and test the binary. - build-test: - needs: - - generate-matrix - strategy: - fail-fast: false - matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }} - runs-on: ${{ matrix.architecture.runner }} - container: ${{ inputs.os == 'linux' && format('ghcr.io/xrplf/ci/{0}-{1}:{2}-{3}', matrix.os.distro_name, matrix.os.distro_version, matrix.os.compiler_name, matrix.os.compiler_version) || null }} - steps: - - name: Check strategy matrix - run: | - echo 'Operating system distro name: ${{ matrix.os.distro_name }}' - echo 'Operating system distro version: ${{ matrix.os.distro_version }}' - echo 'Operating system compiler name: ${{ matrix.os.compiler_name }}' - echo 'Operating system compiler version: ${{ matrix.os.compiler_version }}' - echo 'Architecture platform: ${{ matrix.architecture.platform }}' - echo 'Architecture runner: ${{ toJson(matrix.architecture.runner) }}' - echo 'Build type: ${{ matrix.build_type }}' - echo 'Build only: ${{ matrix.build_only }}' - echo 'CMake arguments: ${{ matrix.cmake_args }}' - echo 'CMake target: ${{ matrix.cmake_target }}' - echo 'Config name: ${{ matrix.config_name }}' - - - name: Cleanup workspace - if: ${{ runner.os == 'macOS' }} - uses: XRPLF/actions/.github/actions/cleanup-workspace@3f044c7478548e3c32ff68980eeb36ece02b364e - - - name: Checkout repository - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - - name: Prepare runner - uses: XRPLF/actions/.github/actions/prepare-runner@638e0dc11ea230f91bd26622fb542116bb5254d5 - with: - disable_ccache: false - - - name: Check configuration (Windows) - if: ${{ inputs.os == 'windows' }} - run: | - echo 'Checking environment variables.' - set - - echo 'Checking CMake version.' - cmake --version - - echo 'Checking Conan version.' - conan --version - - name: Check configuration (Linux and MacOS) - if: ${{ inputs.os == 'linux' || inputs.os == 'macos' }} - run: | - echo 'Checking path.' - echo ${PATH} | tr ':' '\n' - - echo 'Checking environment variables.' - env | sort - - echo 'Checking CMake version.' - cmake --version - - echo 'Checking compiler version.' - ${{ inputs.os == 'linux' && '${CC}' || 'clang' }} --version - - echo 'Checking Conan version.' - conan --version - - echo 'Checking Ninja version.' - ninja --version - - echo 'Checking nproc version.' - nproc --version - - - name: Setup Conan - uses: ./.github/actions/setup-conan - - - name: Build dependencies - uses: ./.github/actions/build-deps - with: - build_dir: ${{ inputs.build_dir }} - build_type: ${{ matrix.build_type }} - force_build: ${{ inputs.dependencies_force_build }} - - - name: Build and test binary - uses: ./.github/actions/build-test - with: - build_dir: ${{ inputs.build_dir }} - build_only: ${{ matrix.build_only }} - build_type: ${{ matrix.build_type }} - cmake_args: ${{ matrix.cmake_args }} - cmake_target: ${{ matrix.cmake_target }} - codecov_token: ${{ secrets.codecov_token }} - os: ${{ inputs.os }} diff --git a/.github/workflows/on-pr.yml b/.github/workflows/on-pr.yml index 24f27d5162..47323ee4a7 100644 --- a/.github/workflows/on-pr.yml +++ b/.github/workflows/on-pr.yml @@ -50,8 +50,8 @@ jobs: files: | # These paths are unique to `on-pr.yml`. .github/scripts/levelization/** - .github/workflows/check-levelization.yml - .github/workflows/notify-clio.yml + .github/workflows/reusable-check-levelization.yml + .github/workflows/reusable-notify-clio.yml .github/workflows/on-pr.yml # Keep the paths below in sync with those in `on-trigger.yml`. @@ -59,8 +59,11 @@ jobs: .github/actions/build-test/** .github/actions/setup-conan/** .github/scripts/strategy-matrix/** - .github/workflows/build-test.yml + .github/workflows/reusable-build.yml + .github/workflows/reusable-build-test-config.yml + .github/workflows/reusable-build-test.yml .github/workflows/reusable-strategy-matrix.yml + .github/workflows/reusable-test.yml .codecov.yml cmake/** conan/** @@ -92,27 +95,27 @@ jobs: check-levelization: needs: should-run - if: needs.should-run.outputs.go == 'true' - uses: ./.github/workflows/check-levelization.yml + if: ${{ needs.should-run.outputs.go == 'true' }} + uses: ./.github/workflows/reusable-check-levelization.yml build-test: needs: should-run - if: needs.should-run.outputs.go == 'true' - uses: ./.github/workflows/build-test.yml + if: ${{ needs.should-run.outputs.go == 'true' }} + uses: ./.github/workflows/reusable-build-test.yml strategy: matrix: os: [linux, macos, windows] with: os: ${{ matrix.os }} secrets: - codecov_token: ${{ secrets.CODECOV_TOKEN }} + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} notify-clio: needs: - should-run - build-test - if: needs.should-run.outputs.go == 'true' - uses: ./.github/workflows/notify-clio.yml + if: ${{ needs.should-run.outputs.go == 'true' && contains(fromJSON('["release", "master"]'), github.ref_name) }} + uses: ./.github/workflows/reusable-notify-clio.yml secrets: clio_notify_token: ${{ secrets.CLIO_NOTIFY_TOKEN }} conan_remote_username: ${{ secrets.CONAN_REMOTE_USERNAME }} diff --git a/.github/workflows/on-trigger.yml b/.github/workflows/on-trigger.yml index 7c17621d67..b06d475a4d 100644 --- a/.github/workflows/on-trigger.yml +++ b/.github/workflows/on-trigger.yml @@ -14,7 +14,7 @@ on: - master paths: # These paths are unique to `on-trigger.yml`. - - ".github/workflows/check-missing-commits.yml" + - ".github/workflows/reusable-check-missing-commits.yml" - ".github/workflows/on-trigger.yml" - ".github/workflows/publish-docs.yml" @@ -23,8 +23,11 @@ on: - ".github/actions/build-test/**" - ".github/actions/setup-conan/**" - ".github/scripts/strategy-matrix/**" - - ".github/workflows/build-test.yml" + - ".github/workflows/reusable-build.yml" + - ".github/workflows/reusable-build-test-config.yml" + - ".github/workflows/reusable-build-test.yml" - ".github/workflows/reusable-strategy-matrix.yml" + - ".github/workflows/reusable-test.yml" - ".codecov.yml" - "cmake/**" - "conan/**" @@ -43,22 +46,8 @@ on: schedule: - cron: "32 6 * * 1-5" - # Run when manually triggered via the GitHub UI or API. If `force_upload` is - # true, then the dependencies that were missing (`force_rebuild` is false) or - # rebuilt (`force_rebuild` is true) will be uploaded, overwriting existing - # dependencies if needed. + # Run when manually triggered via the GitHub UI or API. workflow_dispatch: - inputs: - dependencies_force_build: - description: "Force building of all dependencies." - required: false - type: boolean - default: false - dependencies_force_upload: - description: "Force uploading of all dependencies." - required: false - type: boolean - default: false concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -71,15 +60,15 @@ defaults: jobs: check-missing-commits: if: ${{ github.event_name == 'push' && github.ref_type == 'branch' && contains(fromJSON('["develop", "release"]'), github.ref_name) }} - uses: ./.github/workflows/check-missing-commits.yml + uses: ./.github/workflows/reusable-check-missing-commits.yml build-test: - uses: ./.github/workflows/build-test.yml + uses: ./.github/workflows/reusable-build-test.yml strategy: matrix: os: [linux, macos, windows] with: os: ${{ matrix.os }} - strategy_matrix: "minimal" + strategy_matrix: ${{ github.event_name == 'schedule' && 'all' || 'minimal' }} secrets: - codecov_token: ${{ secrets.CODECOV_TOKEN }} + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index ead137308d..9b85a3bd11 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -7,8 +7,9 @@ on: workflow_dispatch: jobs: + # Call the workflow in the XRPLF/actions repo that runs the pre-commit hooks. run-hooks: uses: XRPLF/actions/.github/workflows/pre-commit.yml@af1b0f0d764cda2e5435f5ac97b240d4bd4d95d3 with: runs_on: ubuntu-latest - container: '{ "image": "ghcr.io/xrplf/ci/tools-rippled-pre-commit" }' + container: '{ "image": "ghcr.io/xrplf/ci/tools-rippled-pre-commit:sha-d1496b8" }' diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index 2fcdd581d1..efd89a5b22 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -27,7 +27,7 @@ env: jobs: publish: runs-on: ubuntu-latest - container: ghcr.io/xrplf/ci/tools-rippled-documentation + container: ghcr.io/xrplf/ci/tools-rippled-documentation:sha-d1496b8 permissions: contents: write steps: diff --git a/.github/workflows/reusable-build-test-config.yml b/.github/workflows/reusable-build-test-config.yml new file mode 100644 index 0000000000..3160cef031 --- /dev/null +++ b/.github/workflows/reusable-build-test-config.yml @@ -0,0 +1,69 @@ +name: Build and test configuration + +on: + workflow_call: + inputs: + build_dir: + description: "The directory where to build." + required: true + type: string + build_only: + description: 'Whether to only build or to build and test the code ("true", "false").' + required: true + type: boolean + build_type: + description: 'The build type to use ("Debug", "Release").' + type: string + required: true + cmake_args: + description: "Additional arguments to pass to CMake." + required: false + type: string + default: "" + cmake_target: + description: "The CMake target to build." + type: string + required: true + + runs_on: + description: Runner to run the job on as a JSON string + required: true + type: string + image: + description: "The image to run in (leave empty to run natively)" + required: true + type: string + + config_name: + description: "The configuration string (used for naming artifacts and such)." + required: true + type: string + + secrets: + CODECOV_TOKEN: + description: "The Codecov token to use for uploading coverage reports." + required: true + +jobs: + build: + uses: ./.github/workflows/reusable-build.yml + with: + build_dir: ${{ inputs.build_dir }} + build_type: ${{ inputs.build_type }} + cmake_args: ${{ inputs.cmake_args }} + cmake_target: ${{ inputs.cmake_target }} + runs_on: ${{ inputs.runs_on }} + image: ${{ inputs.image }} + config_name: ${{ inputs.config_name }} + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + test: + needs: build + uses: ./.github/workflows/reusable-test.yml + with: + run_tests: ${{ !inputs.build_only }} + verify_voidstar: ${{ contains(inputs.cmake_args, '-Dvoidstar=ON') }} + runs_on: ${{ inputs.runs_on }} + image: ${{ inputs.image }} + config_name: ${{ inputs.config_name }} diff --git a/.github/workflows/reusable-build-test.yml b/.github/workflows/reusable-build-test.yml new file mode 100644 index 0000000000..c274cf2b21 --- /dev/null +++ b/.github/workflows/reusable-build-test.yml @@ -0,0 +1,58 @@ +# This workflow builds and tests the binary for various configurations. +name: Build and test + +# This workflow can only be triggered by other workflows. Note that the +# workflow_call event does not support the 'choice' input type, see +# https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#onworkflow_callinputsinput_idtype, +# so we use 'string' instead. +on: + workflow_call: + inputs: + build_dir: + description: "The directory where to build." + required: false + type: string + default: ".build" + os: + description: 'The operating system to use for the build ("linux", "macos", "windows").' + required: true + type: string + strategy_matrix: + # TODO: Support additional strategies, e.g. "ubuntu" for generating all Ubuntu configurations. + description: 'The strategy matrix to use for generating the configurations ("minimal", "all").' + required: false + type: string + default: "minimal" + secrets: + CODECOV_TOKEN: + description: "The Codecov token to use for uploading coverage reports." + required: true + +jobs: + # Generate the strategy matrix to be used by the following job. + generate-matrix: + uses: ./.github/workflows/reusable-strategy-matrix.yml + with: + os: ${{ inputs.os }} + strategy_matrix: ${{ inputs.strategy_matrix }} + + # Build and test the binary for each configuration. + build-test-config: + needs: + - generate-matrix + uses: ./.github/workflows/reusable-build-test-config.yml + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }} + max-parallel: 10 + with: + build_dir: ${{ inputs.build_dir }} + build_only: ${{ matrix.build_only }} + build_type: ${{ matrix.build_type }} + cmake_args: ${{ matrix.cmake_args }} + cmake_target: ${{ matrix.cmake_target }} + runs_on: ${{ toJSON(matrix.architecture.runner) }} + image: ${{ contains(matrix.architecture.platform, 'linux') && format('ghcr.io/xrplf/ci/{0}-{1}:{2}-{3}-sha-5dd7158', matrix.os.distro_name, matrix.os.distro_version, matrix.os.compiler_name, matrix.os.compiler_version) || '' }} + config_name: ${{ matrix.config_name }} + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/reusable-build.yml b/.github/workflows/reusable-build.yml new file mode 100644 index 0000000000..9c994598c2 --- /dev/null +++ b/.github/workflows/reusable-build.yml @@ -0,0 +1,121 @@ +name: Build rippled + +on: + workflow_call: + inputs: + build_dir: + description: "The directory where to build." + required: true + type: string + build_type: + description: 'The build type to use ("Debug", "Release").' + required: true + type: string + cmake_args: + description: "Additional arguments to pass to CMake." + required: true + type: string + cmake_target: + description: "The CMake target to build." + required: true + type: string + + runs_on: + description: Runner to run the job on as a JSON string + required: true + type: string + image: + description: "The image to run in (leave empty to run natively)" + required: true + type: string + + config_name: + description: "The name of the configuration." + required: true + type: string + + secrets: + CODECOV_TOKEN: + description: "The Codecov token to use for uploading coverage reports." + required: true + +defaults: + run: + shell: bash + +jobs: + build: + name: Build ${{ inputs.config_name }} + runs-on: ${{ fromJSON(inputs.runs_on) }} + container: ${{ inputs.image != '' && inputs.image || null }} + steps: + - name: Cleanup workspace + if: ${{ runner.os == 'macOS' }} + uses: XRPLF/actions/.github/actions/cleanup-workspace@3f044c7478548e3c32ff68980eeb36ece02b364e + + - name: Checkout repository + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + + - name: Prepare runner + uses: XRPLF/actions/.github/actions/prepare-runner@638e0dc11ea230f91bd26622fb542116bb5254d5 + with: + disable_ccache: false + + - name: Print build environment + uses: ./.github/actions/print-env + + - name: Setup Conan + uses: ./.github/actions/setup-conan + + - name: Build dependencies + uses: ./.github/actions/build-deps + with: + build_dir: ${{ inputs.build_dir }} + build_type: ${{ inputs.build_type }} + + - name: Configure CMake + shell: bash + working-directory: ${{ inputs.build_dir }} + env: + BUILD_TYPE: ${{ inputs.build_type }} + CMAKE_ARGS: ${{ inputs.cmake_args }} + run: | + cmake \ + -G '${{ runner.os == 'Windows' && 'Visual Studio 17 2022' || 'Ninja' }}' \ + -DCMAKE_TOOLCHAIN_FILE:FILEPATH=build/generators/conan_toolchain.cmake \ + -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} \ + ${{ env.CMAKE_ARGS }} \ + .. + + - name: Build the binary + shell: bash + working-directory: ${{ inputs.build_dir }} + env: + BUILD_TYPE: ${{ inputs.build_type }} + CMAKE_TARGET: ${{ inputs.cmake_target }} + run: | + cmake \ + --build . \ + --config ${{ env.BUILD_TYPE }} \ + --parallel $(nproc) \ + --target ${{ env.CMAKE_TARGET }} + + - name: Upload rippled artifact + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: rippled-${{ inputs.config_name }} + path: ${{ inputs.build_dir }}/${{ runner.os == 'Windows' && inputs.build_type || '' }}/rippled${{ runner.os == 'Windows' && '.exe' || '' }} + retention-days: 3 + if-no-files-found: error + + - name: Upload coverage report + if: ${{ inputs.cmake_target == 'coverage' }} + uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 + with: + disable_search: true + disable_telem: true + fail_ci_if_error: true + files: ${{ inputs.build_dir }}/coverage.xml + plugins: noop + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true diff --git a/.github/workflows/check-levelization.yml b/.github/workflows/reusable-check-levelization.yml similarity index 100% rename from .github/workflows/check-levelization.yml rename to .github/workflows/reusable-check-levelization.yml diff --git a/.github/workflows/check-missing-commits.yml b/.github/workflows/reusable-check-missing-commits.yml similarity index 100% rename from .github/workflows/check-missing-commits.yml rename to .github/workflows/reusable-check-missing-commits.yml diff --git a/.github/workflows/notify-clio.yml b/.github/workflows/reusable-notify-clio.yml similarity index 83% rename from .github/workflows/notify-clio.yml rename to .github/workflows/reusable-notify-clio.yml index 692904ff12..99009d953e 100644 --- a/.github/workflows/notify-clio.yml +++ b/.github/workflows/reusable-notify-clio.yml @@ -40,47 +40,50 @@ jobs: upload: if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} runs-on: ubuntu-latest - container: ghcr.io/xrplf/ci/ubuntu-noble:gcc-13 + container: ghcr.io/xrplf/ci/ubuntu-noble:gcc-13-sha-5dd7158 steps: - name: Checkout repository uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - name: Generate outputs id: generate + env: + PR_NUMBER: ${{ github.event.pull_request.number }} run: | echo 'Generating user and channel.' echo "user=clio" >> "${GITHUB_OUTPUT}" - echo "channel=pr_${{ github.event.pull_request.number }}" >> "${GITHUB_OUTPUT}" + echo "channel=pr_${{ env.PR_NUMBER }}" >> "${GITHUB_OUTPUT}" echo 'Extracting version.' echo "version=$(cat src/libxrpl/protocol/BuildInfo.cpp | grep "versionString =" | awk -F '"' '{print $2}')" >> "${GITHUB_OUTPUT}" - name: Calculate conan reference id: conan_ref run: | echo "conan_ref=${{ steps.generate.outputs.version }}@${{ steps.generate.outputs.user }}/${{ steps.generate.outputs.channel }}" >> "${GITHUB_OUTPUT}" - - name: Set up Conan uses: ./.github/actions/setup-conan with: conan_remote_name: ${{ inputs.conan_remote_name }} conan_remote_url: ${{ inputs.conan_remote_url }} - - name: Log into Conan remote run: conan remote login ${{ inputs.conan_remote_name }} "${{ secrets.conan_remote_username }}" --password "${{ secrets.conan_remote_password }}" - name: Upload package + env: + CONAN_REMOTE_NAME: ${{ inputs.conan_remote_name }} run: | conan export --user=${{ steps.generate.outputs.user }} --channel=${{ steps.generate.outputs.channel }} . - conan upload --confirm --check --remote=${{ inputs.conan_remote_name }} xrpl/${{ steps.conan_ref.outputs.conan_ref }} + conan upload --confirm --check --remote=${{ env.CONAN_REMOTE_NAME }} xrpl/${{ steps.conan_ref.outputs.conan_ref }} outputs: conan_ref: ${{ steps.conan_ref.outputs.conan_ref }} notify: needs: upload runs-on: ubuntu-latest - env: - GH_TOKEN: ${{ secrets.clio_notify_token }} steps: - name: Notify Clio + env: + GH_TOKEN: ${{ secrets.clio_notify_token }} + PR_URL: ${{ github.event.pull_request.html_url }} run: | gh api --method POST -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" \ /repos/xrplf/clio/dispatches -f "event_type=check_libxrpl" \ -F "client_payload[conan_ref]=${{ needs.upload.outputs.conan_ref }}" \ - -F "client_payload[pr_url]=${{ github.event.pull_request.html_url }}" + -F "client_payload[pr_url]=${{ env.PR_URL }}" diff --git a/.github/workflows/reusable-strategy-matrix.yml b/.github/workflows/reusable-strategy-matrix.yml index 20a90fc2e3..e8621527c9 100644 --- a/.github/workflows/reusable-strategy-matrix.yml +++ b/.github/workflows/reusable-strategy-matrix.yml @@ -35,4 +35,7 @@ jobs: - name: Generate strategy matrix working-directory: .github/scripts/strategy-matrix id: generate - run: ./generate.py ${{ inputs.strategy_matrix == 'all' && '--all' || '' }} ${{ inputs.os != '' && format('--config={0}.json', inputs.os) || '' }} >> "${GITHUB_OUTPUT}" + env: + GENERATE_CONFIG: ${{ inputs.os != '' && format('--config={0}.json', inputs.os) || '' }} + GENERATE_OPTION: ${{ inputs.strategy_matrix == 'all' && '--all' || '' }} + run: ./generate.py ${{ env.GENERATE_OPTION }} ${{ env.GENERATE_CONFIG }} >> "${GITHUB_OUTPUT}" diff --git a/.github/workflows/reusable-test.yml b/.github/workflows/reusable-test.yml new file mode 100644 index 0000000000..1877a19a72 --- /dev/null +++ b/.github/workflows/reusable-test.yml @@ -0,0 +1,69 @@ +name: Test rippled + +on: + workflow_call: + inputs: + verify_voidstar: + description: "Whether to verify the presence of voidstar instrumentation." + required: true + type: boolean + run_tests: + description: "Whether to run unit tests" + required: true + type: boolean + + runs_on: + description: Runner to run the job on as a JSON string + required: true + type: string + image: + description: "The image to run in (leave empty to run natively)" + required: true + type: string + + config_name: + description: "The name of the configuration." + required: true + type: string + +jobs: + test: + name: Test ${{ inputs.config_name }} + runs-on: ${{ fromJSON(inputs.runs_on) }} + container: ${{ inputs.image != '' && inputs.image || null }} + steps: + - name: Download rippled artifact + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: rippled-${{ inputs.config_name }} + + - name: Make binary executable (Linux and macOS) + shell: bash + if: ${{ runner.os == 'Linux' || runner.os == 'macOS' }} + run: | + chmod +x ./rippled + + - name: Check linking (Linux) + if: ${{ runner.os == 'Linux' }} + shell: bash + run: | + ldd ./rippled + if [ "$(ldd ./rippled | grep -E '(libstdc\+\+|libgcc)' | wc -l)" -eq 0 ]; then + echo 'The binary is statically linked.' + else + echo 'The binary is dynamically linked.' + exit 1 + fi + + - name: Verifying presence of instrumentation + if: ${{ inputs.verify_voidstar }} + shell: bash + run: | + ./rippled --version | grep libvoidstar + + - name: Test the binary + if: ${{ inputs.run_tests }} + shell: bash + run: | + ./rippled --unittest --unittest-jobs $(nproc) + ctest -j $(nproc) --output-on-failure diff --git a/.github/workflows/upload-conan-deps.yml b/.github/workflows/upload-conan-deps.yml index 6b94815284..cbae8a4c86 100644 --- a/.github/workflows/upload-conan-deps.yml +++ b/.github/workflows/upload-conan-deps.yml @@ -24,13 +24,10 @@ on: branches: [develop] paths: - .github/workflows/upload-conan-deps.yml - - .github/workflows/reusable-strategy-matrix.yml - - .github/actions/build-deps/action.yml - .github/actions/setup-conan/action.yml - ".github/scripts/strategy-matrix/**" - - conanfile.py - conan.lock @@ -56,7 +53,7 @@ jobs: matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }} max-parallel: 10 runs-on: ${{ matrix.architecture.runner }} - container: ${{ contains(matrix.architecture.platform, 'linux') && format('ghcr.io/xrplf/ci/{0}-{1}:{2}-{3}', matrix.os.distro_name, matrix.os.distro_version, matrix.os.compiler_name, matrix.os.compiler_version) || null }} + container: ${{ contains(matrix.architecture.platform, 'linux') && format('ghcr.io/xrplf/ci/{0}-{1}:{2}-{3}-sha-5dd7158', matrix.os.distro_name, matrix.os.distro_version, matrix.os.compiler_name, matrix.os.compiler_version) || null }} steps: - name: Cleanup workspace @@ -83,9 +80,11 @@ jobs: force_build: ${{ github.event_name == 'schedule' || github.event.inputs.force_source_build == 'true' }} - name: Log into Conan remote - if: github.repository_owner == 'XRPLF' && github.event_name != 'pull_request' + if: ${{ github.repository_owner == 'XRPLF' && github.event_name != 'pull_request' }} run: conan remote login ${{ env.CONAN_REMOTE_NAME }} "${{ secrets.CONAN_REMOTE_USERNAME }}" --password "${{ secrets.CONAN_REMOTE_PASSWORD }}" - name: Upload Conan packages - if: github.repository_owner == 'XRPLF' && github.event_name != 'pull_request' && github.event_name != 'schedule' - run: conan upload "*" -r=${{ env.CONAN_REMOTE_NAME }} --confirm ${{ github.event.inputs.force_upload == 'true' && '--force' || '' }} + if: ${{ github.repository_owner == 'XRPLF' && github.event_name != 'pull_request' && github.event_name != 'schedule' }} + env: + FORCE_OPTION: ${{ github.event.inputs.force_upload == 'true' && '--force' || '' }} + run: conan upload "*" --remote='${{ env.CONAN_REMOTE_NAME }}' --confirm ${{ env.FORCE_OPTION }} diff --git a/cmake/RippledCompiler.cmake b/cmake/RippledCompiler.cmake index 4d16222cbe..bc3a62a48c 100644 --- a/cmake/RippledCompiler.cmake +++ b/cmake/RippledCompiler.cmake @@ -16,16 +16,13 @@ set(CMAKE_CXX_EXTENSIONS OFF) target_compile_definitions (common INTERFACE $<$:DEBUG _DEBUG> - #[===[ - NOTE: CMAKE release builds already have NDEBUG defined, so no need to add it - explicitly except for the special case of (profile ON) and (assert OFF). - Presumably this is because we don't want profile builds asserting unless - asserts were specifically requested. - ]===] - $<$,$>>:NDEBUG> - # TODO: Remove once we have migrated functions from OpenSSL 1.x to 3.x. - OPENSSL_SUPPRESS_DEPRECATED -) + $<$,$>>:NDEBUG>) + # ^^^^ NOTE: CMAKE release builds already have NDEBUG + # defined, so no need to add it explicitly except for + # this special case of (profile ON) and (assert OFF) + # -- presumably this is because we don't want profile + # builds asserting unless asserts were specifically + # requested if (MSVC) # remove existing exception flag since we set it to -EHa diff --git a/cmake/RippledCore.cmake b/cmake/RippledCore.cmake index 7d3561675a..481b6e3cea 100644 --- a/cmake/RippledCore.cmake +++ b/cmake/RippledCore.cmake @@ -111,6 +111,12 @@ target_link_libraries(xrpl.libxrpl.net PUBLIC add_module(xrpl server) target_link_libraries(xrpl.libxrpl.server PUBLIC xrpl.libxrpl.protocol) +add_module(xrpl ledger) +target_link_libraries(xrpl.libxrpl.ledger PUBLIC + xrpl.libxrpl.basics + xrpl.libxrpl.json + xrpl.libxrpl.protocol +) add_library(xrpl.libxrpl) set_target_properties(xrpl.libxrpl PROPERTIES OUTPUT_NAME xrpl) @@ -131,6 +137,7 @@ target_link_modules(xrpl PUBLIC resource server net + ledger ) # All headers in libxrpl are in modules. diff --git a/cmake/RippledCov.cmake b/cmake/RippledCov.cmake index 847915a51a..6cbe2ff921 100644 --- a/cmake/RippledCov.cmake +++ b/cmake/RippledCov.cmake @@ -33,7 +33,7 @@ setup_target_for_coverage_gcovr( FORMAT ${coverage_format} EXECUTABLE rippled EXECUTABLE_ARGS --unittest$<$:=${coverage_test}> --unittest-jobs ${coverage_test_parallelism} --quiet --unittest-log - EXCLUDE "src/test" "include/xrpl/beast/test" "include/xrpl/beast/unit_test" "${CMAKE_BINARY_DIR}/pb-xrpl.libpb" + EXCLUDE "src/test" "src/tests" "include/xrpl/beast/test" "include/xrpl/beast/unit_test" "${CMAKE_BINARY_DIR}/pb-xrpl.libpb" DEPENDENCIES rippled ) diff --git a/cmake/RippledInstall.cmake b/cmake/RippledInstall.cmake index f32781f596..95c25a212f 100644 --- a/cmake/RippledInstall.cmake +++ b/cmake/RippledInstall.cmake @@ -18,6 +18,7 @@ install ( xrpl.libxrpl.json xrpl.libxrpl.protocol xrpl.libxrpl.resource + xrpl.libxrpl.ledger xrpl.libxrpl.server xrpl.libxrpl.net xrpl.libxrpl diff --git a/conan.lock b/conan.lock index 0f11f086b4..ec790e16ce 100644 --- a/conan.lock +++ b/conan.lock @@ -9,7 +9,7 @@ "rocksdb/10.0.1#85537f46e538974d67da0c3977de48ac%1756234304.347", "re2/20230301#dfd6e2bf050eb90ddd8729cfb4c844a4%1756234257.976", "protobuf/3.21.12#d927114e28de9f4691a6bbcdd9a529d1%1756234251.614", - "openssl/3.5.2#0c5a5e15ae569f45dff57adcf1770cf7%1756234259.61", + "openssl/1.1.1w#a8f0792d7c5121b954578a7149d23e03%1756223730.729", "nudb/2.0.9#c62cfd501e57055a7e0d8ee3d5e5427d%1756234237.107", "lz4/1.10.0#59fc63cac7f10fbe8e05c7e62c2f3504%1756234228.999", "libiconv/1.17#1e65319e945f2d31941a9d28cc13c058%1756223727.64", diff --git a/conanfile.py b/conanfile.py index 01f61c5d4e..3146b887e0 100644 --- a/conanfile.py +++ b/conanfile.py @@ -27,7 +27,7 @@ class Xrpl(ConanFile): 'grpc/1.50.1', 'libarchive/3.8.1', 'nudb/2.0.9', - 'openssl/3.5.2', + 'openssl/1.1.1w', 'soci/4.0.3', 'zlib/1.3.1', ] diff --git a/include/xrpl/basics/base_uint.h b/include/xrpl/basics/base_uint.h index d36bf74c54..b1a4622cc4 100644 --- a/include/xrpl/basics/base_uint.h +++ b/include/xrpl/basics/base_uint.h @@ -632,6 +632,16 @@ to_string(base_uint const& a) return strHex(a.cbegin(), a.cend()); } +template +inline std::string +to_short_string(base_uint const& a) +{ + static_assert( + base_uint::bytes > 4, + "For 4 bytes or less, use a native type"); + return strHex(a.cbegin(), a.cbegin() + 4) + "..."; +} + template inline std::ostream& operator<<(std::ostream& out, base_uint const& u) diff --git a/include/xrpl/basics/safe_cast.h b/include/xrpl/basics/safe_cast.h index f193f1793f..a750a3b6fc 100644 --- a/include/xrpl/basics/safe_cast.h +++ b/include/xrpl/basics/safe_cast.h @@ -28,9 +28,8 @@ namespace ripple { // the destination can hold all values of the source. This is particularly // handy when the source or destination is an enumeration type. -template -static constexpr bool is_safetocasttovalue_v = - (std::is_integral_v && std::is_integral_v) && +template +concept SafeToCast = (std::is_integral_v && std::is_integral_v) && (std::is_signed::value || std::is_unsigned::value) && (std::is_signed::value != std::is_signed::value ? sizeof(Dest) > sizeof(Src) @@ -78,7 +77,7 @@ inline constexpr std:: unsafe_cast(Src s) noexcept { static_assert( - !is_safetocasttovalue_v, + !SafeToCast, "Only unsafe if casting signed to unsigned or " "destination is too small"); return static_cast(s); diff --git a/include/xrpl/beast/utility/instrumentation.h b/include/xrpl/beast/utility/instrumentation.h index 72c48959a0..3594855eef 100644 --- a/include/xrpl/beast/utility/instrumentation.h +++ b/include/xrpl/beast/utility/instrumentation.h @@ -39,11 +39,16 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. #endif #define XRPL_ASSERT ALWAYS_OR_UNREACHABLE +#define XRPL_ASSERT_PARTS(cond, function, description, ...) \ + XRPL_ASSERT(cond, function " : " description) // How to use the instrumentation macros: // // * XRPL_ASSERT if cond must be true but the line might not be reached during // fuzzing. Same like `assert` in normal use. +// * XRPL_ASSERT_PARTS is for convenience, and works like XRPL_ASSERT, but +// splits the message param into "function" and "description", then joins +// them with " : " before passing to XRPL_ASSERT. // * ALWAYS if cond must be true _and_ the line must be reached during fuzzing. // Same like `assert` in normal use. // * REACHABLE if the line must be reached during fuzzing diff --git a/src/xrpld/ledger/ApplyView.h b/include/xrpl/ledger/ApplyView.h similarity index 99% rename from src/xrpld/ledger/ApplyView.h rename to include/xrpl/ledger/ApplyView.h index 1e4a5a112a..d8b9028d7c 100644 --- a/src/xrpld/ledger/ApplyView.h +++ b/include/xrpl/ledger/ApplyView.h @@ -20,11 +20,10 @@ #ifndef RIPPLE_LEDGER_APPLYVIEW_H_INCLUDED #define RIPPLE_LEDGER_APPLYVIEW_H_INCLUDED -#include -#include - #include #include +#include +#include namespace ripple { diff --git a/src/xrpld/ledger/ApplyViewImpl.h b/include/xrpl/ledger/ApplyViewImpl.h similarity index 97% rename from src/xrpld/ledger/ApplyViewImpl.h rename to include/xrpl/ledger/ApplyViewImpl.h index d170cf71ff..eadcd8acb5 100644 --- a/src/xrpld/ledger/ApplyViewImpl.h +++ b/include/xrpl/ledger/ApplyViewImpl.h @@ -20,9 +20,8 @@ #ifndef RIPPLE_LEDGER_APPLYVIEWIMPL_H_INCLUDED #define RIPPLE_LEDGER_APPLYVIEWIMPL_H_INCLUDED -#include -#include - +#include +#include #include #include diff --git a/src/xrpld/ledger/BookDirs.h b/include/xrpl/ledger/BookDirs.h similarity index 98% rename from src/xrpld/ledger/BookDirs.h rename to include/xrpl/ledger/BookDirs.h index dc58905c5a..95cd41e044 100644 --- a/src/xrpld/ledger/BookDirs.h +++ b/include/xrpl/ledger/BookDirs.h @@ -20,9 +20,8 @@ #ifndef RIPPLE_LEDGER_BOOK_DIRS_H_INCLUDED #define RIPPLE_LEDGER_BOOK_DIRS_H_INCLUDED -#include - #include +#include namespace ripple { diff --git a/src/xrpld/ledger/CachedSLEs.h b/include/xrpl/ledger/CachedSLEs.h similarity index 100% rename from src/xrpld/ledger/CachedSLEs.h rename to include/xrpl/ledger/CachedSLEs.h diff --git a/src/xrpld/ledger/CachedView.h b/include/xrpl/ledger/CachedView.h similarity index 98% rename from src/xrpld/ledger/CachedView.h rename to include/xrpl/ledger/CachedView.h index ae59312f98..3e1fb9cc72 100644 --- a/src/xrpld/ledger/CachedView.h +++ b/include/xrpl/ledger/CachedView.h @@ -20,10 +20,9 @@ #ifndef RIPPLE_LEDGER_CACHEDVIEW_H_INCLUDED #define RIPPLE_LEDGER_CACHEDVIEW_H_INCLUDED -#include -#include - #include +#include +#include #include #include diff --git a/src/xrpld/app/misc/CredentialHelpers.h b/include/xrpl/ledger/CredentialHelpers.h similarity index 98% rename from src/xrpld/app/misc/CredentialHelpers.h rename to include/xrpl/ledger/CredentialHelpers.h index 84938180ce..cb6b3c98ee 100644 --- a/src/xrpld/app/misc/CredentialHelpers.h +++ b/include/xrpl/ledger/CredentialHelpers.h @@ -20,12 +20,11 @@ #ifndef RIPPLE_APP_MISC_CREDENTIALHELPERS_H_INCLUDED #define RIPPLE_APP_MISC_CREDENTIALHELPERS_H_INCLUDED -#include -#include - #include #include #include +#include +#include #include #include #include diff --git a/src/xrpld/ledger/Dir.h b/include/xrpl/ledger/Dir.h similarity index 98% rename from src/xrpld/ledger/Dir.h rename to include/xrpl/ledger/Dir.h index d3a52558fd..0e9e5e998f 100644 --- a/src/xrpld/ledger/Dir.h +++ b/include/xrpl/ledger/Dir.h @@ -20,8 +20,7 @@ #ifndef RIPPLE_LEDGER_DIR_H_INCLUDED #define RIPPLE_LEDGER_DIR_H_INCLUDED -#include - +#include #include namespace ripple { diff --git a/src/xrpld/ledger/OpenView.h b/include/xrpl/ledger/OpenView.h similarity index 98% rename from src/xrpld/ledger/OpenView.h rename to include/xrpl/ledger/OpenView.h index a1fa195a69..e856e03764 100644 --- a/src/xrpld/ledger/OpenView.h +++ b/include/xrpl/ledger/OpenView.h @@ -20,10 +20,9 @@ #ifndef RIPPLE_LEDGER_OPENVIEW_H_INCLUDED #define RIPPLE_LEDGER_OPENVIEW_H_INCLUDED -#include -#include -#include - +#include +#include +#include #include #include diff --git a/src/xrpld/ledger/PaymentSandbox.h b/include/xrpl/ledger/PaymentSandbox.h similarity index 98% rename from src/xrpld/ledger/PaymentSandbox.h rename to include/xrpl/ledger/PaymentSandbox.h index 2cd31ea490..c5bf28d1f9 100644 --- a/src/xrpld/ledger/PaymentSandbox.h +++ b/include/xrpl/ledger/PaymentSandbox.h @@ -20,10 +20,9 @@ #ifndef RIPPLE_LEDGER_PAYMENTSANDBOX_H_INCLUDED #define RIPPLE_LEDGER_PAYMENTSANDBOX_H_INCLUDED -#include -#include -#include - +#include +#include +#include #include #include diff --git a/src/xrpld/ledger/RawView.h b/include/xrpl/ledger/RawView.h similarity index 98% rename from src/xrpld/ledger/RawView.h rename to include/xrpl/ledger/RawView.h index fb6dcffe06..c22fd40132 100644 --- a/src/xrpld/ledger/RawView.h +++ b/include/xrpl/ledger/RawView.h @@ -20,8 +20,7 @@ #ifndef RIPPLE_LEDGER_RAWVIEW_H_INCLUDED #define RIPPLE_LEDGER_RAWVIEW_H_INCLUDED -#include - +#include #include #include diff --git a/src/xrpld/ledger/ReadView.h b/include/xrpl/ledger/ReadView.h similarity index 98% rename from src/xrpld/ledger/ReadView.h rename to include/xrpl/ledger/ReadView.h index 4c1986be4e..2c87dbbf6a 100644 --- a/src/xrpld/ledger/ReadView.h +++ b/include/xrpl/ledger/ReadView.h @@ -20,10 +20,9 @@ #ifndef RIPPLE_LEDGER_READVIEW_H_INCLUDED #define RIPPLE_LEDGER_READVIEW_H_INCLUDED -#include - #include #include +#include #include #include #include @@ -280,6 +279,6 @@ makeRulesGivenLedger( } // namespace ripple -#include +#include #endif diff --git a/src/xrpld/ledger/Sandbox.h b/include/xrpl/ledger/Sandbox.h similarity index 95% rename from src/xrpld/ledger/Sandbox.h rename to include/xrpl/ledger/Sandbox.h index 22b8dbecfb..495da120c6 100644 --- a/src/xrpld/ledger/Sandbox.h +++ b/include/xrpl/ledger/Sandbox.h @@ -20,8 +20,8 @@ #ifndef RIPPLE_LEDGER_SANDBOX_H_INCLUDED #define RIPPLE_LEDGER_SANDBOX_H_INCLUDED -#include -#include +#include +#include namespace ripple { diff --git a/src/xrpld/ledger/View.h b/include/xrpl/ledger/View.h similarity index 96% rename from src/xrpld/ledger/View.h rename to include/xrpl/ledger/View.h index cfd3599f78..9698b4fda3 100644 --- a/src/xrpld/ledger/View.h +++ b/include/xrpl/ledger/View.h @@ -20,11 +20,10 @@ #ifndef RIPPLE_LEDGER_VIEW_H_INCLUDED #define RIPPLE_LEDGER_VIEW_H_INCLUDED -#include -#include -#include - #include +#include +#include +#include #include #include #include @@ -562,12 +561,28 @@ createPseudoAccount( [[nodiscard]] bool isPseudoAccount(std::shared_ptr sleAcct); +// Returns the list of fields that define an ACCOUNT_ROOT as a pseudo-account if +// set +// Pseudo-account designator fields MUST be maintained by including the +// SField::sMD_PseudoAccount flag in the SField definition. (Don't forget to +// "| SField::sMD_Default"!) The fields do NOT need to be amendment-gated, +// since a non-active amendment will not set any field, by definition. +// Specific properties of a pseudo-account are NOT checked here, that's what +// InvariantCheck is for. +[[nodiscard]] std::vector const& +getPseudoAccountFields(); + [[nodiscard]] inline bool isPseudoAccount(ReadView const& view, AccountID accountId) { return isPseudoAccount(view.read(keylet::account(accountId))); } +[[nodiscard]] TER +canAddHolding(ReadView const& view, Asset const& asset); + +/// Any transactors that call addEmptyHolding() in doApply must call +/// canAddHolding() in preflight with the same View and Asset [[nodiscard]] TER addEmptyHolding( ApplyView& view, @@ -719,7 +734,8 @@ rippleUnlockEscrowMPT( ApplyView& view, AccountID const& uGrantorID, AccountID const& uGranteeID, - STAmount const& saAmount, + STAmount const& netAmount, + STAmount const& grossAmount, beast::Journal j); /** Calls static accountSendIOU if saAmount represents Issue. diff --git a/src/xrpld/ledger/detail/ApplyStateTable.h b/include/xrpl/ledger/detail/ApplyStateTable.h similarity index 97% rename from src/xrpld/ledger/detail/ApplyStateTable.h rename to include/xrpl/ledger/detail/ApplyStateTable.h index 5a2e0bcf54..01ab1e07ab 100644 --- a/src/xrpld/ledger/detail/ApplyStateTable.h +++ b/include/xrpl/ledger/detail/ApplyStateTable.h @@ -20,11 +20,10 @@ #ifndef RIPPLE_LEDGER_APPLYSTATETABLE_H_INCLUDED #define RIPPLE_LEDGER_APPLYSTATETABLE_H_INCLUDED -#include -#include -#include - #include +#include +#include +#include #include #include #include diff --git a/src/xrpld/ledger/detail/ApplyViewBase.h b/include/xrpl/ledger/detail/ApplyViewBase.h similarity index 96% rename from src/xrpld/ledger/detail/ApplyViewBase.h rename to include/xrpl/ledger/detail/ApplyViewBase.h index f9c9c80af0..af86fc23b4 100644 --- a/src/xrpld/ledger/detail/ApplyViewBase.h +++ b/include/xrpl/ledger/detail/ApplyViewBase.h @@ -20,10 +20,9 @@ #ifndef RIPPLE_LEDGER_APPLYVIEWBASE_H_INCLUDED #define RIPPLE_LEDGER_APPLYVIEWBASE_H_INCLUDED -#include -#include -#include - +#include +#include +#include #include namespace ripple { diff --git a/src/xrpld/ledger/detail/RawStateTable.h b/include/xrpl/ledger/detail/RawStateTable.h similarity index 98% rename from src/xrpld/ledger/detail/RawStateTable.h rename to include/xrpl/ledger/detail/RawStateTable.h index 37597aa678..53a6f52208 100644 --- a/src/xrpld/ledger/detail/RawStateTable.h +++ b/include/xrpl/ledger/detail/RawStateTable.h @@ -20,8 +20,8 @@ #ifndef RIPPLE_LEDGER_RAWSTATETABLE_H_INCLUDED #define RIPPLE_LEDGER_RAWSTATETABLE_H_INCLUDED -#include -#include +#include +#include #include #include diff --git a/src/xrpld/ledger/detail/ReadViewFwdRange.h b/include/xrpl/ledger/detail/ReadViewFwdRange.h similarity index 100% rename from src/xrpld/ledger/detail/ReadViewFwdRange.h rename to include/xrpl/ledger/detail/ReadViewFwdRange.h diff --git a/src/xrpld/ledger/detail/ReadViewFwdRange.ipp b/include/xrpl/ledger/detail/ReadViewFwdRange.ipp similarity index 100% rename from src/xrpld/ledger/detail/ReadViewFwdRange.ipp rename to include/xrpl/ledger/detail/ReadViewFwdRange.ipp diff --git a/include/xrpl/protocol/FeeUnits.h b/include/xrpl/protocol/FeeUnits.h deleted file mode 100644 index 31a1886b7f..0000000000 --- a/include/xrpl/protocol/FeeUnits.h +++ /dev/null @@ -1,565 +0,0 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2019 Ripple Labs Inc. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ - -#ifndef BASICS_FEES_H_INCLUDED -#define BASICS_FEES_H_INCLUDED - -#include -#include -#include -#include - -#include -#include - -#include -#include -#include - -namespace ripple { - -namespace feeunit { - -/** "drops" are the smallest divisible amount of XRP. This is what most - of the code uses. */ -struct dropTag; -/** "fee units" calculations are a not-really-unitless value that is used - to express the cost of a given transaction vs. a reference transaction. - They are primarily used by the Transactor classes. */ -struct feeunitTag; -/** "fee levels" are used by the transaction queue to compare the relative - cost of transactions that require different levels of effort to process. - See also: src/ripple/app/misc/FeeEscalation.md#fee-level */ -struct feelevelTag; -/** unitless values are plain scalars wrapped in a TaggedFee. They are - used for calculations in this header. */ -struct unitlessTag; - -template -using enable_if_unit_t = typename std::enable_if_t< - std::is_class_v && std::is_object_v && - std::is_object_v>; - -/** `is_usable_unit_v` is checked to ensure that only values with - known valid type tags can be used (sometimes transparently) in - non-fee contexts. At the time of implementation, this includes - all known tags, but more may be added in the future, and they - should not be added automatically unless determined to be - appropriate. -*/ -template > -constexpr bool is_usable_unit_v = - std::is_same_v || - std::is_same_v || - std::is_same_v || - std::is_same_v; - -template -class TaggedFee : private boost::totally_ordered>, - private boost::additive>, - private boost::equality_comparable, T>, - private boost::dividable, T>, - private boost::modable, T>, - private boost::unit_steppable> -{ -public: - using unit_type = UnitTag; - using value_type = T; - -private: - value_type fee_; - -protected: - template - static constexpr bool is_compatible_v = - std::is_arithmetic_v && std::is_arithmetic_v && - std::is_convertible_v; - - template > - static constexpr bool is_compatiblefee_v = - is_compatible_v && - std::is_same_v; - - template - using enable_if_compatible_t = - typename std::enable_if_t>; - - template - using enable_if_compatiblefee_t = - typename std::enable_if_t>; - -public: - TaggedFee() = default; - constexpr TaggedFee(TaggedFee const& other) = default; - constexpr TaggedFee& - operator=(TaggedFee const& other) = default; - - constexpr explicit TaggedFee(beast::Zero) : fee_(0) - { - } - - constexpr TaggedFee& - operator=(beast::Zero) - { - fee_ = 0; - return *this; - } - - constexpr explicit TaggedFee(value_type fee) : fee_(fee) - { - } - - TaggedFee& - operator=(value_type fee) - { - fee_ = fee; - return *this; - } - - /** Instances with the same unit, and a type that is - "safe" to convert to this one can be converted - implicitly */ - template < - class Other, - class = std::enable_if_t< - is_compatible_v && - is_safetocasttovalue_v>> - constexpr TaggedFee(TaggedFee const& fee) - : TaggedFee(safe_cast(fee.fee())) - { - } - - constexpr TaggedFee - operator*(value_type const& rhs) const - { - return TaggedFee{fee_ * rhs}; - } - - friend constexpr TaggedFee - operator*(value_type lhs, TaggedFee const& rhs) - { - // multiplication is commutative - return rhs * lhs; - } - - constexpr value_type - operator/(TaggedFee const& rhs) const - { - return fee_ / rhs.fee_; - } - - TaggedFee& - operator+=(TaggedFee const& other) - { - fee_ += other.fee(); - return *this; - } - - TaggedFee& - operator-=(TaggedFee const& other) - { - fee_ -= other.fee(); - return *this; - } - - TaggedFee& - operator++() - { - ++fee_; - return *this; - } - - TaggedFee& - operator--() - { - --fee_; - return *this; - } - - TaggedFee& - operator*=(value_type const& rhs) - { - fee_ *= rhs; - return *this; - } - - TaggedFee& - operator/=(value_type const& rhs) - { - fee_ /= rhs; - return *this; - } - - template - std::enable_if_t, TaggedFee&> - operator%=(value_type const& rhs) - { - fee_ %= rhs; - return *this; - } - - TaggedFee - operator-() const - { - static_assert( - std::is_signed_v, "- operator illegal on unsigned fee types"); - return TaggedFee{-fee_}; - } - - bool - operator==(TaggedFee const& other) const - { - return fee_ == other.fee_; - } - - template > - bool - operator==(TaggedFee const& other) const - { - return fee_ == other.fee(); - } - - bool - operator==(value_type other) const - { - return fee_ == other; - } - - template > - bool - operator!=(TaggedFee const& other) const - { - return !operator==(other); - } - - bool - operator<(TaggedFee const& other) const - { - return fee_ < other.fee_; - } - - /** Returns true if the amount is not zero */ - explicit constexpr - operator bool() const noexcept - { - return fee_ != 0; - } - - /** Return the sign of the amount */ - constexpr int - signum() const noexcept - { - return (fee_ < 0) ? -1 : (fee_ ? 1 : 0); - } - - /** Returns the number of drops */ - constexpr value_type - fee() const - { - return fee_; - } - - template - constexpr double - decimalFromReference(TaggedFee reference) const - { - return static_cast(fee_) / reference.fee(); - } - - // `is_usable_unit_v` is checked to ensure that only values with - // known valid type tags can be converted to JSON. At the time - // of implementation, that includes all known tags, but more may - // be added in the future. - std::enable_if_t, Json::Value> - jsonClipped() const - { - if constexpr (std::is_integral_v) - { - using jsontype = std::conditional_t< - std::is_signed_v, - Json::Int, - Json::UInt>; - - constexpr auto min = std::numeric_limits::min(); - constexpr auto max = std::numeric_limits::max(); - - if (fee_ < min) - return min; - if (fee_ > max) - return max; - return static_cast(fee_); - } - else - { - return fee_; - } - } - - /** Returns the underlying value. Code SHOULD NOT call this - function unless the type has been abstracted away, - e.g. in a templated function. - */ - constexpr value_type - value() const - { - return fee_; - } - - friend std::istream& - operator>>(std::istream& s, TaggedFee& val) - { - s >> val.fee_; - return s; - } -}; - -// Output Fees as just their numeric value. -template -std::basic_ostream& -operator<<(std::basic_ostream& os, TaggedFee const& q) -{ - return os << q.value(); -} - -template -std::string -to_string(TaggedFee const& amount) -{ - return std::to_string(amount.fee()); -} - -template > -constexpr bool can_muldiv_source_v = - std::is_convertible_v; - -template > -constexpr bool can_muldiv_dest_v = - can_muldiv_source_v && // Dest is also a source - std::is_convertible_v && - sizeof(typename Dest::value_type) >= sizeof(std::uint64_t); - -template < - class Source1, - class Source2, - class = enable_if_unit_t, - class = enable_if_unit_t> -constexpr bool can_muldiv_sources_v = - can_muldiv_source_v && can_muldiv_source_v && - std::is_same_v; - -template < - class Source1, - class Source2, - class Dest, - class = enable_if_unit_t, - class = enable_if_unit_t, - class = enable_if_unit_t> -constexpr bool can_muldiv_v = - can_muldiv_sources_v && can_muldiv_dest_v; -// Source and Dest can be the same by default - -template < - class Source1, - class Source2, - class Dest, - class = enable_if_unit_t, - class = enable_if_unit_t, - class = enable_if_unit_t> -constexpr bool can_muldiv_commute_v = can_muldiv_v && - !std::is_same_v; - -template -using enable_muldiv_source_t = - typename std::enable_if_t>; - -template -using enable_muldiv_dest_t = typename std::enable_if_t>; - -template -using enable_muldiv_sources_t = - typename std::enable_if_t>; - -template -using enable_muldiv_t = - typename std::enable_if_t>; - -template -using enable_muldiv_commute_t = - typename std::enable_if_t>; - -template -TaggedFee -scalar(T value) -{ - return TaggedFee{value}; -} - -template < - class Source1, - class Source2, - class Dest, - class = enable_muldiv_t> -std::optional -mulDivU(Source1 value, Dest mul, Source2 div) -{ - // Fees can never be negative in any context. - if (value.value() < 0 || mul.value() < 0 || div.value() < 0) - { - // split the asserts so if one hits, the user can tell which - // without a debugger. - XRPL_ASSERT( - value.value() >= 0, - "ripple::feeunit::mulDivU : minimum value input"); - XRPL_ASSERT( - mul.value() >= 0, "ripple::feeunit::mulDivU : minimum mul input"); - XRPL_ASSERT( - div.value() >= 0, "ripple::feeunit::mulDivU : minimum div input"); - return std::nullopt; - } - - using desttype = typename Dest::value_type; - constexpr auto max = std::numeric_limits::max(); - - // Shortcuts, since these happen a lot in the real world - if (value == div) - return mul; - if (mul.value() == div.value()) - { - if (value.value() > max) - return std::nullopt; - return Dest{static_cast(value.value())}; - } - - using namespace boost::multiprecision; - - uint128_t product; - product = multiply( - product, - static_cast(value.value()), - static_cast(mul.value())); - - auto quotient = product / div.value(); - - if (quotient > max) - return std::nullopt; - - return Dest{static_cast(quotient)}; -} - -} // namespace feeunit - -template -using FeeLevel = feeunit::TaggedFee; -using FeeLevel64 = FeeLevel; -using FeeLevelDouble = FeeLevel; - -template < - class Source1, - class Source2, - class Dest, - class = feeunit::enable_muldiv_t> -std::optional -mulDiv(Source1 value, Dest mul, Source2 div) -{ - return feeunit::mulDivU(value, mul, div); -} - -template < - class Source1, - class Source2, - class Dest, - class = feeunit::enable_muldiv_commute_t> -std::optional -mulDiv(Dest value, Source1 mul, Source2 div) -{ - // Multiplication is commutative - return feeunit::mulDivU(mul, value, div); -} - -template > -std::optional -mulDiv(std::uint64_t value, Dest mul, std::uint64_t div) -{ - // Give the scalars a non-tag so the - // unit-handling version gets called. - return feeunit::mulDivU(feeunit::scalar(value), mul, feeunit::scalar(div)); -} - -template > -std::optional -mulDiv(Dest value, std::uint64_t mul, std::uint64_t div) -{ - // Multiplication is commutative - return mulDiv(mul, value, div); -} - -template < - class Source1, - class Source2, - class = feeunit::enable_muldiv_sources_t> -std::optional -mulDiv(Source1 value, std::uint64_t mul, Source2 div) -{ - // Give the scalars a dimensionless unit so the - // unit-handling version gets called. - auto unitresult = feeunit::mulDivU(value, feeunit::scalar(mul), div); - - if (!unitresult) - return std::nullopt; - - return unitresult->value(); -} - -template < - class Source1, - class Source2, - class = feeunit::enable_muldiv_sources_t> -std::optional -mulDiv(std::uint64_t value, Source1 mul, Source2 div) -{ - // Multiplication is commutative - return mulDiv(mul, value, div); -} - -template -constexpr std::enable_if_t< - std::is_same_v && - std::is_integral_v && - std::is_integral_v, - Dest> -safe_cast(Src s) noexcept -{ - // Dest may not have an explicit value constructor - return Dest{safe_cast(s.value())}; -} - -template -constexpr std::enable_if_t< - std::is_same_v && - std::is_integral_v && - std::is_integral_v, - Dest> -unsafe_cast(Src s) noexcept -{ - // Dest may not have an explicit value constructor - return Dest{unsafe_cast(s.value())}; -} - -} // namespace ripple - -#endif // BASICS_FEES_H_INCLUDED diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index 3e3f2843c1..79be15d906 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -287,9 +287,11 @@ delegate(AccountID const& account, AccountID const& authorizedAccount) noexcept; Keylet bridge(STXChainBridge const& bridge, STXChainBridge::ChainType chainType); +// `seq` is stored as `sfXChainClaimID` in the object Keylet xChainClaimID(STXChainBridge const& bridge, std::uint64_t seq); +// `seq` is stored as `sfXChainAccountCreateCount` in the object Keylet xChainCreateAccountClaimID(STXChainBridge const& bridge, std::uint64_t seq); diff --git a/include/xrpl/protocol/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h index f4eae88f3c..5ea470269a 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -56,7 +56,7 @@ enum LedgerEntryType : std::uint16_t #pragma push_macro("LEDGER_ENTRY") #undef LEDGER_ENTRY -#define LEDGER_ENTRY(tag, value, name, rpcName, fields) tag = value, +#define LEDGER_ENTRY(tag, value, ...) tag = value, #include @@ -189,6 +189,15 @@ enum LedgerSpecificFlags { lsfMPTCanClawback = 0x00000040, lsfMPTNoConfidentialTransfer = 0x00000080, + lsmfMPTCanMutateCanLock = 0x00000002, + lsmfMPTCanMutateRequireAuth = 0x00000004, + lsmfMPTCanMutateCanEscrow = 0x00000008, + lsmfMPTCanMutateCanTrade = 0x00000010, + lsmfMPTCanMutateCanTransfer = 0x00000020, + lsmfMPTCanMutateCanClawback = 0x00000040, + lsmfMPTCanMutateMetadata = 0x00010000, + lsmfMPTCanMutateTransferFee = 0x00020000, + // ltMPTOKEN lsfMPTAuthorized = 0x00000002, diff --git a/include/xrpl/protocol/Permissions.h b/include/xrpl/protocol/Permissions.h index cf49ff7382..d3f5253cd0 100644 --- a/include/xrpl/protocol/Permissions.h +++ b/include/xrpl/protocol/Permissions.h @@ -74,6 +74,9 @@ public: Permission& operator=(Permission const&) = delete; + std::optional + getPermissionName(std::uint32_t const value) const; + std::optional getGranularValue(std::string const& name) const; @@ -83,6 +86,9 @@ public: std::optional getGranularTxType(GranularPermissionType const& gpType) const; + std::optional> const + getTxFeature(TxType txType) const; + bool isDelegatable(std::uint32_t const& permissionValue, Rules const& rules) const; diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index ae3d3563a6..a0895ff0d9 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -22,7 +22,6 @@ #include #include -#include #include diff --git a/include/xrpl/protocol/SField.h b/include/xrpl/protocol/SField.h index 777cfa02ba..b6ae98b48f 100644 --- a/include/xrpl/protocol/SField.h +++ b/include/xrpl/protocol/SField.h @@ -22,6 +22,7 @@ #include #include +#include #include #include @@ -71,8 +72,10 @@ class STCurrency; STYPE(STI_VL, 7) \ STYPE(STI_ACCOUNT, 8) \ STYPE(STI_NUMBER, 9) \ + STYPE(STI_INT32, 10) \ + STYPE(STI_INT64, 11) \ \ - /* 10-13 are reserved */ \ + /* 12-13 are reserved */ \ STYPE(STI_OBJECT, 14) \ STYPE(STI_ARRAY, 15) \ \ @@ -148,8 +151,10 @@ public: sMD_ChangeNew = 0x02, // new value when it changes sMD_DeleteFinal = 0x04, // final value when it is deleted sMD_Create = 0x08, // value when it's created - sMD_Always = 0x10, // value when node containing it is affected at all - sMD_BaseTen = 0x20, + sMD_Always = 0x10, // value when node containing it is affected at all + sMD_BaseTen = 0x20, // value is treated as base 10, overriding behavior + sMD_PseudoAccount = 0x40, // if this field is set in an ACCOUNT_ROOT + // _only_, then it is a pseudo-account sMD_Default = sMD_ChangeOrig | sMD_ChangeNew | sMD_DeleteFinal | sMD_Create }; @@ -184,7 +189,7 @@ public: char const* fn, int meta = sMD_Default, IsSigning signing = IsSigning::yes); - explicit SField(private_access_tag_t, int fc); + explicit SField(private_access_tag_t, int fc, char const* fn); static SField const& getField(int fieldCode); @@ -297,7 +302,7 @@ public: static int compare(SField const& f1, SField const& f2); - static std::map const& + static std::unordered_map const& getKnownCodeToField() { return knownCodeToField; @@ -305,7 +310,8 @@ public: private: static int num; - static std::map knownCodeToField; + static std::unordered_map knownCodeToField; + static std::unordered_map knownNameToField; }; /** A field with a type known at compile time. */ @@ -352,6 +358,9 @@ using SF_UINT256 = TypedField>; using SF_UINT384 = TypedField>; using SF_UINT512 = TypedField>; +using SF_INT32 = TypedField>; +using SF_INT64 = TypedField>; + using SF_ACCOUNT = TypedField; using SF_AMOUNT = TypedField; using SF_ISSUE = TypedField; diff --git a/include/xrpl/protocol/STInteger.h b/include/xrpl/protocol/STInteger.h index b259638774..154ee7f203 100644 --- a/include/xrpl/protocol/STInteger.h +++ b/include/xrpl/protocol/STInteger.h @@ -81,6 +81,8 @@ using STUInt16 = STInteger; using STUInt32 = STInteger; using STUInt64 = STInteger; +using STInt32 = STInteger; + template inline STInteger::STInteger(Integer v) : value_(v) { diff --git a/include/xrpl/protocol/STLedgerEntry.h b/include/xrpl/protocol/STLedgerEntry.h index 3609a04d4b..571c7af5fe 100644 --- a/include/xrpl/protocol/STLedgerEntry.h +++ b/include/xrpl/protocol/STLedgerEntry.h @@ -26,7 +26,9 @@ namespace ripple { class Rules; +namespace test { class Invariants_test; +} class STLedgerEntry final : public STObject, public CountedObject { @@ -36,6 +38,8 @@ class STLedgerEntry final : public STObject, public CountedObject public: using pointer = std::shared_ptr; using ref = std::shared_ptr const&; + using const_pointer = std::shared_ptr; + using const_ref = std::shared_ptr const&; /** Create an empty object with the given key and type. */ explicit STLedgerEntry(Keylet const& k); @@ -54,7 +58,7 @@ public: getText() const override; Json::Value - getJson(JsonOptions options) const override; + getJson(JsonOptions options = JsonOptions::none) const override; /** Returns the 'key' (or 'index') of this item. The key identifies this entry's position in @@ -84,7 +88,8 @@ private: void setSLEType(); - friend Invariants_test; // this test wants access to the private type_ + friend test::Invariants_test; // this test wants access to the private + // type_ STBase* copy(std::size_t n, void* buf) const override; diff --git a/include/xrpl/protocol/STObject.h b/include/xrpl/protocol/STObject.h index 6cd083ef85..1c22b08aba 100644 --- a/include/xrpl/protocol/STObject.h +++ b/include/xrpl/protocol/STObject.h @@ -25,7 +25,6 @@ #include #include #include -#include #include #include #include @@ -34,6 +33,7 @@ #include #include #include +#include #include #include @@ -231,6 +231,8 @@ public: getFieldH192(SField const& field) const; uint256 getFieldH256(SField const& field) const; + std::int32_t + getFieldI32(SField const& field) const; AccountID getAccountID(SField const& field) const; @@ -365,6 +367,8 @@ public: void setFieldH256(SField const& field, uint256 const&); void + setFieldI32(SField const& field, std::int32_t); + void setFieldVL(SField const& field, Blob const&); void setFieldVL(SField const& field, Slice const&); diff --git a/include/xrpl/protocol/STParsedJSON.h b/include/xrpl/protocol/STParsedJSON.h index d655969030..9c770fe94d 100644 --- a/include/xrpl/protocol/STParsedJSON.h +++ b/include/xrpl/protocol/STParsedJSON.h @@ -54,34 +54,6 @@ public: Json::Value error; }; -/** Holds the serialized result of parsing an input JSON array. - This does validation and checking on the provided JSON. -*/ -class STParsedJSONArray -{ -public: - /** Parses and creates an STParsedJSON array. - The result of the parsing is stored in array and error. - Exceptions: - Does not throw. - @param name The name of the JSON field, used in diagnostics. - @param json The JSON-RPC to parse. - */ - STParsedJSONArray(std::string const& name, Json::Value const& json); - - STParsedJSONArray() = delete; - STParsedJSONArray(STParsedJSONArray const&) = delete; - STParsedJSONArray& - operator=(STParsedJSONArray const&) = delete; - ~STParsedJSONArray() = default; - - /** The STArray if the parse was successful. */ - std::optional array; - - /** On failure, an appropriate set of error values. */ - Json::Value error; -}; - } // namespace ripple #endif diff --git a/include/xrpl/protocol/STValidation.h b/include/xrpl/protocol/STValidation.h index 2aa74203a2..991922514d 100644 --- a/include/xrpl/protocol/STValidation.h +++ b/include/xrpl/protocol/STValidation.h @@ -22,10 +22,10 @@ #include #include -#include #include #include #include +#include #include #include diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index 9ace6b80f8..0a3a3b999e 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -673,7 +673,8 @@ isTerRetry(TER x) noexcept inline bool isTesSuccess(TER x) noexcept { - return (x == tesSUCCESS); + // Makes use of TERSubset::operator bool() + return !(x); } inline bool diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index 38c6eb27b9..e71b8f0565 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -127,6 +127,8 @@ constexpr std::uint32_t tfTrustSetPermissionMask = ~(tfUniversal | tfSetfAuth | // EnableAmendment flags: constexpr std::uint32_t tfGotMajority = 0x00010000; constexpr std::uint32_t tfLostMajority = 0x00020000; +constexpr std::uint32_t tfChangeMask = + ~( tfUniversal | tfGotMajority | tfLostMajority); // PaymentChannelClaim flags: constexpr std::uint32_t tfRenew = 0x00010000; @@ -141,7 +143,8 @@ constexpr std::uint32_t const tfTransferable = 0x00000008; constexpr std::uint32_t const tfMutable = 0x00000010; // MPTokenIssuanceCreate flags: -// NOTE - there is intentionally no flag here for lsfMPTLocked, which this transaction cannot mutate. +// Note: tf/lsfMPTLocked is intentionally omitted, since this transaction +// is not allowed to modify it. constexpr std::uint32_t const tfMPTCanLock = lsfMPTCanLock; constexpr std::uint32_t const tfMPTRequireAuth = lsfMPTRequireAuth; constexpr std::uint32_t const tfMPTCanEscrow = lsfMPTCanEscrow; @@ -152,6 +155,20 @@ constexpr std::uint32_t const tfMPTNoConfidentialTransfer = lsfMPTNoConfidentia constexpr std::uint32_t const tfMPTokenIssuanceCreateMask = ~(tfUniversal | tfMPTCanLock | tfMPTRequireAuth | tfMPTCanEscrow | tfMPTCanTrade | tfMPTCanTransfer | tfMPTCanClawback | tfMPTNoConfidentialTransfer); +// MPTokenIssuanceCreate MutableFlags: +// Indicating specific fields or flags may be changed after issuance. +constexpr std::uint32_t const tmfMPTCanMutateCanLock = lsmfMPTCanMutateCanLock; +constexpr std::uint32_t const tmfMPTCanMutateRequireAuth = lsmfMPTCanMutateRequireAuth; +constexpr std::uint32_t const tmfMPTCanMutateCanEscrow = lsmfMPTCanMutateCanEscrow; +constexpr std::uint32_t const tmfMPTCanMutateCanTrade = lsmfMPTCanMutateCanTrade; +constexpr std::uint32_t const tmfMPTCanMutateCanTransfer = lsmfMPTCanMutateCanTransfer; +constexpr std::uint32_t const tmfMPTCanMutateCanClawback = lsmfMPTCanMutateCanClawback; +constexpr std::uint32_t const tmfMPTCanMutateMetadata = lsmfMPTCanMutateMetadata; +constexpr std::uint32_t const tmfMPTCanMutateTransferFee = lsmfMPTCanMutateTransferFee; +constexpr std::uint32_t const tmfMPTokenIssuanceCreateMutableMask = + ~(tmfMPTCanMutateCanLock | tmfMPTCanMutateRequireAuth | tmfMPTCanMutateCanEscrow | tmfMPTCanMutateCanTrade + | tmfMPTCanMutateCanTransfer | tmfMPTCanMutateCanClawback | tmfMPTCanMutateMetadata | tmfMPTCanMutateTransferFee); + // MPTokenAuthorize flags: constexpr std::uint32_t const tfMPTUnauthorize = 0x00000001; constexpr std::uint32_t const tfMPTokenAuthorizeMask = ~(tfUniversal | tfMPTUnauthorize); @@ -162,6 +179,25 @@ constexpr std::uint32_t const tfMPTUnlock = 0x00000002; constexpr std::uint32_t const tfMPTokenIssuanceSetMask = ~(tfUniversal | tfMPTLock | tfMPTUnlock); constexpr std::uint32_t const tfMPTokenIssuanceSetPermissionMask = ~(tfUniversal | tfMPTLock | tfMPTUnlock); +// MPTokenIssuanceSet MutableFlags: +// Set or Clear flags. +constexpr std::uint32_t const tmfMPTSetCanLock = 0x00000001; +constexpr std::uint32_t const tmfMPTClearCanLock = 0x00000002; +constexpr std::uint32_t const tmfMPTSetRequireAuth = 0x00000004; +constexpr std::uint32_t const tmfMPTClearRequireAuth = 0x00000008; +constexpr std::uint32_t const tmfMPTSetCanEscrow = 0x00000010; +constexpr std::uint32_t const tmfMPTClearCanEscrow = 0x00000020; +constexpr std::uint32_t const tmfMPTSetCanTrade = 0x00000040; +constexpr std::uint32_t const tmfMPTClearCanTrade = 0x00000080; +constexpr std::uint32_t const tmfMPTSetCanTransfer = 0x00000100; +constexpr std::uint32_t const tmfMPTClearCanTransfer = 0x00000200; +constexpr std::uint32_t const tmfMPTSetCanClawback = 0x00000400; +constexpr std::uint32_t const tmfMPTClearCanClawback = 0x00000800; +constexpr std::uint32_t const tmfMPTokenIssuanceSetMutableMask = ~(tmfMPTSetCanLock | tmfMPTClearCanLock | + tmfMPTSetRequireAuth | tmfMPTClearRequireAuth | tmfMPTSetCanEscrow | tmfMPTClearCanEscrow | + tmfMPTSetCanTrade | tmfMPTClearCanTrade | tmfMPTSetCanTransfer | tmfMPTClearCanTransfer | + tmfMPTSetCanClawback | tmfMPTClearCanClawback); + // MPTokenIssuanceDestroy flags: constexpr std::uint32_t const tfMPTokenIssuanceDestroyMask = ~tfUniversal; diff --git a/include/xrpl/protocol/Units.h b/include/xrpl/protocol/Units.h new file mode 100644 index 0000000000..6b2ae67059 --- /dev/null +++ b/include/xrpl/protocol/Units.h @@ -0,0 +1,555 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2019 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +#ifndef PROTOCOL_UNITS_H_INCLUDED +#define PROTOCOL_UNITS_H_INCLUDED + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +namespace ripple { + +namespace unit { + +/** "drops" are the smallest divisible amount of XRP. This is what most + of the code uses. */ +struct dropTag; +/** "fee levels" are used by the transaction queue to compare the relative + cost of transactions that require different levels of effort to process. + See also: src/ripple/app/misc/FeeEscalation.md#fee-level */ +struct feelevelTag; +/** unitless values are plain scalars wrapped in a ValueUnit. They are + used for calculations in this header. */ +struct unitlessTag; + +/** Units to represent basis points (bips) and 1/10 basis points */ +class BipsTag; +class TenthBipsTag; + +// These names don't have to be too descriptive, because we're in the "unit" +// namespace. + +template +concept Valid = std::is_class_v && std::is_object_v && + std::is_object_v; + +/** `Usable` is checked to ensure that only values with + known valid type tags can be used (sometimes transparently) in + non-unit contexts. At the time of implementation, this includes + all known tags, but more may be added in the future, and they + should not be added automatically unless determined to be + appropriate. +*/ +template +concept Usable = Valid && + (std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v); + +template +concept Compatible = Valid && std::is_arithmetic_v && + std::is_arithmetic_v && + std::is_convertible_v; + +template +concept Integral = std::is_integral_v; + +template +concept IntegralValue = Integral; + +template +concept CastableValue = IntegralValue && IntegralValue && + std::is_same_v; + +template +class ValueUnit : private boost::totally_ordered>, + private boost::additive>, + private boost::equality_comparable, T>, + private boost::dividable, T>, + private boost::modable, T>, + private boost::unit_steppable> +{ +public: + using unit_type = UnitTag; + using value_type = T; + +private: + value_type value_; + +public: + ValueUnit() = default; + constexpr ValueUnit(ValueUnit const& other) = default; + constexpr ValueUnit& + operator=(ValueUnit const& other) = default; + + constexpr explicit ValueUnit(beast::Zero) : value_(0) + { + } + + constexpr ValueUnit& + operator=(beast::Zero) + { + value_ = 0; + return *this; + } + + constexpr explicit ValueUnit(value_type value) : value_(value) + { + } + + constexpr ValueUnit& + operator=(value_type value) + { + value_ = value; + return *this; + } + + /** Instances with the same unit, and a type that is + "safe" to convert to this one can be converted + implicitly */ + template Other> + constexpr ValueUnit(ValueUnit const& value) + requires SafeToCast + : ValueUnit(safe_cast(value.value())) + { + } + + constexpr ValueUnit + operator+(value_type const& rhs) const + { + return ValueUnit{value_ + rhs}; + } + + friend constexpr ValueUnit + operator+(value_type lhs, ValueUnit const& rhs) + { + // addition is commutative + return rhs + lhs; + } + + constexpr ValueUnit + operator-(value_type const& rhs) const + { + return ValueUnit{value_ - rhs}; + } + + friend constexpr ValueUnit + operator-(value_type lhs, ValueUnit const& rhs) + { + // subtraction is NOT commutative, but (lhs + (-rhs)) is addition, which + // is + return -rhs + lhs; + } + + constexpr ValueUnit + operator*(value_type const& rhs) const + { + return ValueUnit{value_ * rhs}; + } + + friend constexpr ValueUnit + operator*(value_type lhs, ValueUnit const& rhs) + { + // multiplication is commutative + return rhs * lhs; + } + + constexpr value_type + operator/(ValueUnit const& rhs) const + { + return value_ / rhs.value_; + } + + ValueUnit& + operator+=(ValueUnit const& other) + { + value_ += other.value(); + return *this; + } + + ValueUnit& + operator-=(ValueUnit const& other) + { + value_ -= other.value(); + return *this; + } + + ValueUnit& + operator++() + { + ++value_; + return *this; + } + + ValueUnit& + operator--() + { + --value_; + return *this; + } + + ValueUnit& + operator*=(value_type const& rhs) + { + value_ *= rhs; + return *this; + } + + ValueUnit& + operator/=(value_type const& rhs) + { + value_ /= rhs; + return *this; + } + + template + ValueUnit& + operator%=(value_type const& rhs) + { + value_ %= rhs; + return *this; + } + + ValueUnit + operator-() const + { + static_assert( + std::is_signed_v, "- operator illegal on unsigned value types"); + return ValueUnit{-value_}; + } + + constexpr bool + operator==(ValueUnit const& other) const + { + return value_ == other.value_; + } + + template Other> + constexpr bool + operator==(ValueUnit const& other) const + { + return value_ == other.value(); + } + + constexpr bool + operator==(value_type other) const + { + return value_ == other; + } + + template Other> + constexpr bool + operator!=(ValueUnit const& other) const + { + return !operator==(other); + } + + constexpr bool + operator<(ValueUnit const& other) const + { + return value_ < other.value_; + } + + /** Returns true if the amount is not zero */ + explicit constexpr + operator bool() const noexcept + { + return value_ != 0; + } + + /** Return the sign of the amount */ + constexpr int + signum() const noexcept + { + return (value_ < 0) ? -1 : (value_ ? 1 : 0); + } + + /** Returns the number of drops */ + // TODO: Move this to a new class, maybe with the old "TaggedFee" name + constexpr value_type + fee() const + { + return value_; + } + + template + constexpr double + decimalFromReference(ValueUnit reference) const + { + return static_cast(value_) / reference.value(); + } + + // `Usable` is checked to ensure that only values with + // known valid type tags can be converted to JSON. At the time + // of implementation, that includes all known tags, but more may + // be added in the future. + Json::Value + jsonClipped() const + requires Usable + { + if constexpr (std::is_integral_v) + { + using jsontype = std::conditional_t< + std::is_signed_v, + Json::Int, + Json::UInt>; + + constexpr auto min = std::numeric_limits::min(); + constexpr auto max = std::numeric_limits::max(); + + if (value_ < min) + return min; + if (value_ > max) + return max; + return static_cast(value_); + } + else + { + return value_; + } + } + + /** Returns the underlying value. Code SHOULD NOT call this + function unless the type has been abstracted away, + e.g. in a templated function. + */ + constexpr value_type + value() const + { + return value_; + } + + friend std::istream& + operator>>(std::istream& s, ValueUnit& val) + { + s >> val.value_; + return s; + } +}; + +// Output Values as just their numeric value. +template +std::basic_ostream& +operator<<(std::basic_ostream& os, ValueUnit const& q) +{ + return os << q.value(); +} + +template +std::string +to_string(ValueUnit const& amount) +{ + return std::to_string(amount.value()); +} + +template +concept muldivSource = Valid && + std::is_convertible_v; + +template +concept muldivDest = muldivSource && // Dest is also a source + std::is_convertible_v && + sizeof(typename Dest::value_type) >= sizeof(std::uint64_t); + +template +concept muldivSources = muldivSource && muldivSource && + std::is_same_v; + +template +concept muldivable = muldivSources && muldivDest; +// Source and Dest can be the same by default + +template +concept muldivCommutable = muldivable && + !std::is_same_v; + +template +ValueUnit +scalar(T value) +{ + return ValueUnit{value}; +} + +template Dest> +std::optional +mulDivU(Source1 value, Dest mul, Source2 div) +{ + // values can never be negative in any context. + if (value.value() < 0 || mul.value() < 0 || div.value() < 0) + { + // split the asserts so if one hits, the user can tell which + // without a debugger. + XRPL_ASSERT( + value.value() >= 0, "ripple::unit::mulDivU : minimum value input"); + XRPL_ASSERT( + mul.value() >= 0, "ripple::unit::mulDivU : minimum mul input"); + XRPL_ASSERT( + div.value() > 0, "ripple::unit::mulDivU : minimum div input"); + return std::nullopt; + } + + using desttype = typename Dest::value_type; + constexpr auto max = std::numeric_limits::max(); + + // Shortcuts, since these happen a lot in the real world + if (value == div) + return mul; + if (mul.value() == div.value()) + { + if (value.value() > max) + return std::nullopt; + return Dest{static_cast(value.value())}; + } + + using namespace boost::multiprecision; + + uint128_t product; + product = multiply( + product, + static_cast(value.value()), + static_cast(mul.value())); + + auto quotient = product / div.value(); + + if (quotient > max) + return std::nullopt; + + return Dest{static_cast(quotient)}; +} + +} // namespace unit + +// Fee Levels +template +using FeeLevel = unit::ValueUnit; +using FeeLevel64 = FeeLevel; +using FeeLevelDouble = FeeLevel; + +// Basis points (Bips) +template +using Bips = unit::ValueUnit; +using Bips16 = Bips; +using Bips32 = Bips; +template +using TenthBips = unit::ValueUnit; +using TenthBips16 = TenthBips; +using TenthBips32 = TenthBips; + +template Dest> +std::optional +mulDiv(Source1 value, Dest mul, Source2 div) +{ + return unit::mulDivU(value, mul, div); +} + +template < + class Source1, + class Source2, + unit::muldivCommutable Dest> +std::optional +mulDiv(Dest value, Source1 mul, Source2 div) +{ + // Multiplication is commutative + return unit::mulDivU(mul, value, div); +} + +template +std::optional +mulDiv(std::uint64_t value, Dest mul, std::uint64_t div) +{ + // Give the scalars a non-tag so the + // unit-handling version gets called. + return unit::mulDivU(unit::scalar(value), mul, unit::scalar(div)); +} + +template +std::optional +mulDiv(Dest value, std::uint64_t mul, std::uint64_t div) +{ + // Multiplication is commutative + return mulDiv(mul, value, div); +} + +template Source2> +std::optional +mulDiv(Source1 value, std::uint64_t mul, Source2 div) +{ + // Give the scalars a dimensionless unit so the + // unit-handling version gets called. + auto unitresult = unit::mulDivU(value, unit::scalar(mul), div); + + if (!unitresult) + return std::nullopt; + + return unitresult->value(); +} + +template Source2> +std::optional +mulDiv(std::uint64_t value, Source1 mul, Source2 div) +{ + // Multiplication is commutative + return mulDiv(mul, value, div); +} + +template Src> +constexpr Dest +safe_cast(Src s) noexcept +{ + // Dest may not have an explicit value constructor + return Dest{safe_cast(s.value())}; +} + +template +constexpr Dest +safe_cast(Src s) noexcept +{ + // Dest may not have an explicit value constructor + return Dest{safe_cast(s)}; +} + +template Src> +constexpr Dest +unsafe_cast(Src s) noexcept +{ + // Dest may not have an explicit value constructor + return Dest{unsafe_cast(s.value())}; +} + +template +constexpr Dest +unsafe_cast(Src s) noexcept +{ + // Dest may not have an explicit value constructor + return Dest{unsafe_cast(s)}; +} + +} // namespace ripple + +#endif // PROTOCOL_UNITS_H_INCLUDED diff --git a/include/xrpl/protocol/XRPAmount.h b/include/xrpl/protocol/XRPAmount.h index 332735dc6f..a7a013d625 100644 --- a/include/xrpl/protocol/XRPAmount.h +++ b/include/xrpl/protocol/XRPAmount.h @@ -24,7 +24,7 @@ #include #include #include -#include +#include #include #include @@ -42,7 +42,7 @@ class XRPAmount : private boost::totally_ordered, private boost::additive { public: - using unit_type = feeunit::dropTag; + using unit_type = unit::dropTag; using value_type = std::int64_t; private: diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index ed25da8c39..0c461960b4 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -33,8 +33,11 @@ // in include/xrpl/protocol/Feature.h. XRPL_FEATURE(ConfidentialTransfer, Supported::no, VoteBehavior::DefaultNo) +XRPL_FIX (IncludeKeyletFields, Supported::yes, VoteBehavior::DefaultNo) +XRPL_FEATURE(DynamicMPT, Supported::no, VoteBehavior::DefaultNo) +XRPL_FIX (TokenEscrowV1, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (DelegateV1_1, Supported::no, VoteBehavior::DefaultNo) -XRPL_FIX (PriceOracleOrder, Supported::no, VoteBehavior::DefaultNo) +XRPL_FIX (PriceOracleOrder, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (MPTDeliveredAmount, Supported::no, VoteBehavior::DefaultNo) XRPL_FIX (AMMClawbackRounding, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(TokenEscrow, Supported::yes, VoteBehavior::DefaultNo) @@ -43,7 +46,7 @@ XRPL_FIX (AMMv1_3, Supported::yes, VoteBehavior::DefaultNo XRPL_FEATURE(PermissionedDEX, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(Batch, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(SingleAssetVault, Supported::no, VoteBehavior::DefaultNo) -XRPL_FEATURE(PermissionDelegation, Supported::yes, VoteBehavior::DefaultNo) +XRPL_FEATURE(PermissionDelegation, Supported::no, VoteBehavior::DefaultNo) XRPL_FIX (PayChanCancelAfter, Supported::yes, VoteBehavior::DefaultNo) // Check flags in Credential transactions XRPL_FIX (InvalidTxFlags, Supported::yes, VoteBehavior::DefaultNo) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 868bc6a88a..c6e1602c5e 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -120,6 +120,7 @@ LEDGER_ENTRY(ltNFTOKEN_PAGE, 0x0050, NFTokenPage, nft_page, ({ // All fields are soeREQUIRED because there is always a SignerEntries. // If there are no SignerEntries the node is deleted. LEDGER_ENTRY(ltSIGNER_LIST, 0x0053, SignerList, signer_list, ({ + {sfOwner, soeOPTIONAL}, {sfOwnerNode, soeREQUIRED}, {sfSignerQuorum, soeREQUIRED}, {sfSignerEntries, soeREQUIRED}, @@ -188,7 +189,7 @@ LEDGER_ENTRY(ltDIR_NODE, 0x0064, DirectoryNode, directory, ({ {sfNFTokenID, soeOPTIONAL}, {sfPreviousTxnID, soeOPTIONAL}, {sfPreviousTxnLgrSeq, soeOPTIONAL}, - {sfDomainID, soeOPTIONAL} + {sfDomainID, soeOPTIONAL} // order book directories })) /** The ledger object which lists details about amendments on the network. @@ -343,6 +344,7 @@ LEDGER_ENTRY(ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID, 0x0074, XChainOwnedCreateAc */ LEDGER_ENTRY(ltESCROW, 0x0075, Escrow, escrow, ({ {sfAccount, soeREQUIRED}, + {sfSequence, soeOPTIONAL}, {sfDestination, soeREQUIRED}, {sfAmount, soeREQUIRED}, {sfCondition, soeOPTIONAL}, @@ -365,6 +367,7 @@ LEDGER_ENTRY(ltESCROW, 0x0075, Escrow, escrow, ({ LEDGER_ENTRY(ltPAYCHAN, 0x0078, PayChannel, payment_channel, ({ {sfAccount, soeREQUIRED}, {sfDestination, soeREQUIRED}, + {sfSequence, soeOPTIONAL}, {sfAmount, soeREQUIRED}, {sfBalance, soeREQUIRED}, {sfPublicKey, soeREQUIRED}, @@ -412,6 +415,7 @@ LEDGER_ENTRY(ltMPTOKEN_ISSUANCE, 0x007e, MPTokenIssuance, mpt_issuance, ({ {sfPreviousTxnID, soeREQUIRED}, {sfPreviousTxnLgrSeq, soeREQUIRED}, {sfDomainID, soeOPTIONAL}, + {sfMutableFlags, soeDEFAULT}, {sfIssuerElGamalPublicKey, soeOPTIONAL}, {sfConfidentialOutstandingAmount, soeOPTIONAL}, })) @@ -439,6 +443,7 @@ LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, mptoken, ({ */ LEDGER_ENTRY(ltORACLE, 0x0080, Oracle, oracle, ({ {sfOwner, soeREQUIRED}, + {sfOracleDocumentID, soeOPTIONAL}, {sfProvider, soeREQUIRED}, {sfPriceDataSeries, soeREQUIRED}, {sfAssetClass, soeREQUIRED}, diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 40c8bd7ddb..741b4f764e 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -114,7 +114,8 @@ TYPED_SFIELD(sfVoteWeight, UINT32, 48) TYPED_SFIELD(sfFirstNFTokenSequence, UINT32, 50) TYPED_SFIELD(sfOracleDocumentID, UINT32, 51) TYPED_SFIELD(sfPermissionValue, UINT32, 52) -TYPED_SFIELD(sfConfidentialBalanceVersion, UINT32, 53) +TYPED_SFIELD(sfMutableFlags, UINT32, 53) +TYPED_SFIELD(sfConfidentialBalanceVersion, UINT32, 54) // 64-bit integers (common) TYPED_SFIELD(sfIndexNext, UINT64, 1) @@ -175,7 +176,8 @@ TYPED_SFIELD(sfNFTokenID, UINT256, 10) TYPED_SFIELD(sfEmitParentTxnID, UINT256, 11) TYPED_SFIELD(sfEmitNonce, UINT256, 12) TYPED_SFIELD(sfEmitHookHash, UINT256, 13) -TYPED_SFIELD(sfAMMID, UINT256, 14) +TYPED_SFIELD(sfAMMID, UINT256, 14, + SField::sMD_PseudoAccount | SField::sMD_Default) // 256-bit (uncommon) TYPED_SFIELD(sfBookDirectory, UINT256, 16) @@ -197,7 +199,8 @@ TYPED_SFIELD(sfHookHash, UINT256, 31) TYPED_SFIELD(sfHookNamespace, UINT256, 32) TYPED_SFIELD(sfHookSetTxnID, UINT256, 33) TYPED_SFIELD(sfDomainID, UINT256, 34) -TYPED_SFIELD(sfVaultID, UINT256, 35) +TYPED_SFIELD(sfVaultID, UINT256, 35, + SField::sMD_PseudoAccount | SField::sMD_Default) TYPED_SFIELD(sfParentBatchID, UINT256, 36) // number (common) @@ -207,6 +210,12 @@ TYPED_SFIELD(sfAssetsMaximum, NUMBER, 3) TYPED_SFIELD(sfAssetsTotal, NUMBER, 4) TYPED_SFIELD(sfLossUnrealized, NUMBER, 5) +// int32 +// NOTE: Do not use `sfDummyInt32`. It's so far the only use of INT32 +// in this file and has been defined here for test only. +// TODO: Replace `sfDummyInt32` with actually useful field. +TYPED_SFIELD(sfDummyInt32, INT32, 1) // for tests only + // currency amount (common) TYPED_SFIELD(sfAmount, AMOUNT, 1) TYPED_SFIELD(sfBalance, AMOUNT, 2) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 63b340bcc8..dbe687e232 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -22,16 +22,31 @@ #endif /** - * TRANSACTION(tag, value, name, delegatable, amendments, fields) + * TRANSACTION(tag, value, name, delegatable, amendments, privileges, fields) + * + * To ease maintenance, you may replace any unneeded values with "..." + * e.g. #define TRANSACTION(tag, value, name, ...) * * You must define a transactor class in the `ripple` namespace named `name`, - * and include its header in `src/xrpld/app/tx/detail/applySteps.cpp`. + * and include its header alongside the TRANSACTOR definition using this + * format: + * #if TRANSACTION_INCLUDE + * # include + * #endif + * + * The `privileges` parameter of the TRANSACTION macro is a bitfield + * defining which operations the transaction can perform. + * The values are defined and used in InvariantCheck.cpp */ /** This transaction type executes a payment. */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttPAYMENT, 0, Payment, Delegation::delegatable, uint256{}, + createAcct, ({ {sfDestination, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported}, @@ -45,9 +60,13 @@ TRANSACTION(ttPAYMENT, 0, Payment, })) /** This transaction type creates an escrow object. */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate, Delegation::delegatable, uint256{}, + noPriv, ({ {sfDestination, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported}, @@ -61,6 +80,7 @@ TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate, TRANSACTION(ttESCROW_FINISH, 2, EscrowFinish, Delegation::delegatable, uint256{}, + noPriv, ({ {sfOwner, soeREQUIRED}, {sfOfferSequence, soeREQUIRED}, @@ -71,9 +91,13 @@ TRANSACTION(ttESCROW_FINISH, 2, EscrowFinish, /** This transaction type adjusts various account settings. */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttACCOUNT_SET, 3, AccountSet, Delegation::notDelegatable, uint256{}, + noPriv, ({ {sfEmailHash, soeOPTIONAL}, {sfWalletLocator, soeOPTIONAL}, @@ -88,18 +112,26 @@ TRANSACTION(ttACCOUNT_SET, 3, AccountSet, })) /** This transaction type cancels an existing escrow. */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttESCROW_CANCEL, 4, EscrowCancel, Delegation::delegatable, uint256{}, + noPriv, ({ {sfOwner, soeREQUIRED}, {sfOfferSequence, soeREQUIRED}, })) /** This transaction type sets or clears an account's "regular key". */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttREGULAR_KEY_SET, 5, SetRegularKey, Delegation::notDelegatable, uint256{}, + noPriv, ({ {sfRegularKey, soeOPTIONAL}, })) @@ -107,9 +139,13 @@ TRANSACTION(ttREGULAR_KEY_SET, 5, SetRegularKey, // 6 deprecated /** This transaction type creates an offer to trade one asset for another. */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttOFFER_CREATE, 7, OfferCreate, Delegation::delegatable, uint256{}, + noPriv, ({ {sfTakerPays, soeREQUIRED}, {sfTakerGets, soeREQUIRED}, @@ -119,9 +155,13 @@ TRANSACTION(ttOFFER_CREATE, 7, OfferCreate, })) /** This transaction type cancels existing offers to trade one asset for another. */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttOFFER_CANCEL, 8, OfferCancel, Delegation::delegatable, uint256{}, + noPriv, ({ {sfOfferSequence, soeREQUIRED}, })) @@ -129,9 +169,13 @@ TRANSACTION(ttOFFER_CANCEL, 8, OfferCancel, // 9 deprecated /** This transaction type creates a new set of tickets. */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttTICKET_CREATE, 10, TicketCreate, Delegation::delegatable, featureTicketBatch, + noPriv, ({ {sfTicketCount, soeREQUIRED}, })) @@ -141,18 +185,26 @@ TRANSACTION(ttTICKET_CREATE, 10, TicketCreate, /** This transaction type modifies the signer list associated with an account. */ // The SignerEntries are optional because a SignerList is deleted by // setting the SignerQuorum to zero and omitting SignerEntries. +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttSIGNER_LIST_SET, 12, SignerListSet, Delegation::notDelegatable, uint256{}, + noPriv, ({ {sfSignerQuorum, soeREQUIRED}, {sfSignerEntries, soeOPTIONAL}, })) /** This transaction type creates a new unidirectional XRP payment channel. */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttPAYCHAN_CREATE, 13, PaymentChannelCreate, Delegation::delegatable, uint256{}, + noPriv, ({ {sfDestination, soeREQUIRED}, {sfAmount, soeREQUIRED}, @@ -166,6 +218,7 @@ TRANSACTION(ttPAYCHAN_CREATE, 13, PaymentChannelCreate, TRANSACTION(ttPAYCHAN_FUND, 14, PaymentChannelFund, Delegation::delegatable, uint256{}, + noPriv, ({ {sfChannel, soeREQUIRED}, {sfAmount, soeREQUIRED}, @@ -176,6 +229,7 @@ TRANSACTION(ttPAYCHAN_FUND, 14, PaymentChannelFund, TRANSACTION(ttPAYCHAN_CLAIM, 15, PaymentChannelClaim, Delegation::delegatable, uint256{}, + noPriv, ({ {sfChannel, soeREQUIRED}, {sfAmount, soeOPTIONAL}, @@ -186,9 +240,13 @@ TRANSACTION(ttPAYCHAN_CLAIM, 15, PaymentChannelClaim, })) /** This transaction type creates a new check. */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttCHECK_CREATE, 16, CheckCreate, Delegation::delegatable, featureChecks, + noPriv, ({ {sfDestination, soeREQUIRED}, {sfSendMax, soeREQUIRED}, @@ -198,9 +256,13 @@ TRANSACTION(ttCHECK_CREATE, 16, CheckCreate, })) /** This transaction type cashes an existing check. */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttCHECK_CASH, 17, CheckCash, Delegation::delegatable, featureChecks, + noPriv, ({ {sfCheckID, soeREQUIRED}, {sfAmount, soeOPTIONAL}, @@ -208,17 +270,25 @@ TRANSACTION(ttCHECK_CASH, 17, CheckCash, })) /** This transaction type cancels an existing check. */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttCHECK_CANCEL, 18, CheckCancel, Delegation::delegatable, featureChecks, + noPriv, ({ {sfCheckID, soeREQUIRED}, })) /** This transaction type grants or revokes authorization to transfer funds. */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttDEPOSIT_PREAUTH, 19, DepositPreauth, Delegation::delegatable, featureDepositPreauth, + noPriv, ({ {sfAuthorize, soeOPTIONAL}, {sfUnauthorize, soeOPTIONAL}, @@ -227,9 +297,13 @@ TRANSACTION(ttDEPOSIT_PREAUTH, 19, DepositPreauth, })) /** This transaction type modifies a trustline between two accounts. */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttTRUST_SET, 20, TrustSet, Delegation::delegatable, uint256{}, + noPriv, ({ {sfLimitAmount, soeOPTIONAL}, {sfQualityIn, soeOPTIONAL}, @@ -237,9 +311,13 @@ TRANSACTION(ttTRUST_SET, 20, TrustSet, })) /** This transaction type deletes an existing account. */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttACCOUNT_DELETE, 21, AccountDelete, Delegation::notDelegatable, uint256{}, + mustDeleteAcct, ({ {sfDestination, soeREQUIRED}, {sfDestinationTag, soeOPTIONAL}, @@ -249,9 +327,13 @@ TRANSACTION(ttACCOUNT_DELETE, 21, AccountDelete, // 22 reserved /** This transaction mints a new NFT. */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttNFTOKEN_MINT, 25, NFTokenMint, Delegation::delegatable, featureNonFungibleTokensV1, + changeNFTCounts, ({ {sfNFTokenTaxon, soeREQUIRED}, {sfTransferFee, soeOPTIONAL}, @@ -263,18 +345,26 @@ TRANSACTION(ttNFTOKEN_MINT, 25, NFTokenMint, })) /** This transaction burns (i.e. destroys) an existing NFT. */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttNFTOKEN_BURN, 26, NFTokenBurn, Delegation::delegatable, featureNonFungibleTokensV1, + changeNFTCounts, ({ {sfNFTokenID, soeREQUIRED}, {sfOwner, soeOPTIONAL}, })) /** This transaction creates a new offer to buy or sell an NFT. */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttNFTOKEN_CREATE_OFFER, 27, NFTokenCreateOffer, Delegation::delegatable, featureNonFungibleTokensV1, + noPriv, ({ {sfNFTokenID, soeREQUIRED}, {sfAmount, soeREQUIRED}, @@ -284,17 +374,25 @@ TRANSACTION(ttNFTOKEN_CREATE_OFFER, 27, NFTokenCreateOffer, })) /** This transaction cancels an existing offer to buy or sell an existing NFT. */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttNFTOKEN_CANCEL_OFFER, 28, NFTokenCancelOffer, Delegation::delegatable, featureNonFungibleTokensV1, + noPriv, ({ {sfNFTokenOffers, soeREQUIRED}, })) /** This transaction accepts an existing offer to buy or sell an existing NFT. */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttNFTOKEN_ACCEPT_OFFER, 29, NFTokenAcceptOffer, Delegation::delegatable, featureNonFungibleTokensV1, + noPriv, ({ {sfNFTokenBuyOffer, soeOPTIONAL}, {sfNFTokenSellOffer, soeOPTIONAL}, @@ -302,18 +400,26 @@ TRANSACTION(ttNFTOKEN_ACCEPT_OFFER, 29, NFTokenAcceptOffer, })) /** This transaction claws back issued tokens. */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttCLAWBACK, 30, Clawback, Delegation::delegatable, featureClawback, + noPriv, ({ {sfAmount, soeREQUIRED, soeMPTSupported}, {sfHolder, soeOPTIONAL}, })) /** This transaction claws back tokens from an AMM pool. */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttAMM_CLAWBACK, 31, AMMClawback, Delegation::delegatable, featureAMMClawback, + mayDeleteAcct | overrideFreeze, ({ {sfHolder, soeREQUIRED}, {sfAsset, soeREQUIRED}, @@ -322,9 +428,13 @@ TRANSACTION(ttAMM_CLAWBACK, 31, AMMClawback, })) /** This transaction type creates an AMM instance */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttAMM_CREATE, 35, AMMCreate, Delegation::delegatable, featureAMM, + createPseudoAcct, ({ {sfAmount, soeREQUIRED}, {sfAmount2, soeREQUIRED}, @@ -332,9 +442,13 @@ TRANSACTION(ttAMM_CREATE, 35, AMMCreate, })) /** This transaction type deposits into an AMM instance */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttAMM_DEPOSIT, 36, AMMDeposit, Delegation::delegatable, featureAMM, + noPriv, ({ {sfAsset, soeREQUIRED}, {sfAsset2, soeREQUIRED}, @@ -346,9 +460,13 @@ TRANSACTION(ttAMM_DEPOSIT, 36, AMMDeposit, })) /** This transaction type withdraws from an AMM instance */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttAMM_WITHDRAW, 37, AMMWithdraw, Delegation::delegatable, featureAMM, + mayDeleteAcct, ({ {sfAsset, soeREQUIRED}, {sfAsset2, soeREQUIRED}, @@ -359,9 +477,13 @@ TRANSACTION(ttAMM_WITHDRAW, 37, AMMWithdraw, })) /** This transaction type votes for the trading fee */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttAMM_VOTE, 38, AMMVote, Delegation::delegatable, featureAMM, + noPriv, ({ {sfAsset, soeREQUIRED}, {sfAsset2, soeREQUIRED}, @@ -369,9 +491,13 @@ TRANSACTION(ttAMM_VOTE, 38, AMMVote, })) /** This transaction type bids for the auction slot */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttAMM_BID, 39, AMMBid, Delegation::delegatable, featureAMM, + noPriv, ({ {sfAsset, soeREQUIRED}, {sfAsset2, soeREQUIRED}, @@ -381,18 +507,26 @@ TRANSACTION(ttAMM_BID, 39, AMMBid, })) /** This transaction type deletes AMM in the empty state */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttAMM_DELETE, 40, AMMDelete, Delegation::delegatable, featureAMM, + mustDeleteAcct, ({ {sfAsset, soeREQUIRED}, {sfAsset2, soeREQUIRED}, })) /** This transactions creates a crosschain sequence number */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttXCHAIN_CREATE_CLAIM_ID, 41, XChainCreateClaimID, Delegation::delegatable, featureXChainBridge, + noPriv, ({ {sfXChainBridge, soeREQUIRED}, {sfSignatureReward, soeREQUIRED}, @@ -403,6 +537,7 @@ TRANSACTION(ttXCHAIN_CREATE_CLAIM_ID, 41, XChainCreateClaimID, TRANSACTION(ttXCHAIN_COMMIT, 42, XChainCommit, Delegation::delegatable, featureXChainBridge, + noPriv, ({ {sfXChainBridge, soeREQUIRED}, {sfXChainClaimID, soeREQUIRED}, @@ -414,6 +549,7 @@ TRANSACTION(ttXCHAIN_COMMIT, 42, XChainCommit, TRANSACTION(ttXCHAIN_CLAIM, 43, XChainClaim, Delegation::delegatable, featureXChainBridge, + noPriv, ({ {sfXChainBridge, soeREQUIRED}, {sfXChainClaimID, soeREQUIRED}, @@ -426,6 +562,7 @@ TRANSACTION(ttXCHAIN_CLAIM, 43, XChainClaim, TRANSACTION(ttXCHAIN_ACCOUNT_CREATE_COMMIT, 44, XChainAccountCreateCommit, Delegation::delegatable, featureXChainBridge, + noPriv, ({ {sfXChainBridge, soeREQUIRED}, {sfDestination, soeREQUIRED}, @@ -437,6 +574,7 @@ TRANSACTION(ttXCHAIN_ACCOUNT_CREATE_COMMIT, 44, XChainAccountCreateCommit, TRANSACTION(ttXCHAIN_ADD_CLAIM_ATTESTATION, 45, XChainAddClaimAttestation, Delegation::delegatable, featureXChainBridge, + createAcct, ({ {sfXChainBridge, soeREQUIRED}, @@ -453,9 +591,11 @@ TRANSACTION(ttXCHAIN_ADD_CLAIM_ATTESTATION, 45, XChainAddClaimAttestation, })) /** This transaction adds an attestation to an account */ -TRANSACTION(ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION, 46, XChainAddAccountCreateAttestation, +TRANSACTION(ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION, 46, + XChainAddAccountCreateAttestation, Delegation::delegatable, featureXChainBridge, + createAcct, ({ {sfXChainBridge, soeREQUIRED}, @@ -476,6 +616,7 @@ TRANSACTION(ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION, 46, XChainAddAccountCreateA TRANSACTION(ttXCHAIN_MODIFY_BRIDGE, 47, XChainModifyBridge, Delegation::delegatable, featureXChainBridge, + noPriv, ({ {sfXChainBridge, soeREQUIRED}, {sfSignatureReward, soeOPTIONAL}, @@ -486,6 +627,7 @@ TRANSACTION(ttXCHAIN_MODIFY_BRIDGE, 47, XChainModifyBridge, TRANSACTION(ttXCHAIN_CREATE_BRIDGE, 48, XChainCreateBridge, Delegation::delegatable, featureXChainBridge, + noPriv, ({ {sfXChainBridge, soeREQUIRED}, {sfSignatureReward, soeREQUIRED}, @@ -493,9 +635,13 @@ TRANSACTION(ttXCHAIN_CREATE_BRIDGE, 48, XChainCreateBridge, })) /** This transaction type creates or updates a DID */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttDID_SET, 49, DIDSet, Delegation::delegatable, featureDID, + noPriv, ({ {sfDIDDocument, soeOPTIONAL}, {sfURI, soeOPTIONAL}, @@ -506,12 +652,17 @@ TRANSACTION(ttDID_SET, 49, DIDSet, TRANSACTION(ttDID_DELETE, 50, DIDDelete, Delegation::delegatable, featureDID, + noPriv, ({})) /** This transaction type creates an Oracle instance */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttORACLE_SET, 51, OracleSet, Delegation::delegatable, featurePriceOracle, + noPriv, ({ {sfOracleDocumentID, soeREQUIRED}, {sfProvider, soeOPTIONAL}, @@ -522,65 +673,97 @@ TRANSACTION(ttORACLE_SET, 51, OracleSet, })) /** This transaction type deletes an Oracle instance */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttORACLE_DELETE, 52, OracleDelete, Delegation::delegatable, featurePriceOracle, + noPriv, ({ {sfOracleDocumentID, soeREQUIRED}, })) /** This transaction type fixes a problem in the ledger state */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttLEDGER_STATE_FIX, 53, LedgerStateFix, Delegation::delegatable, fixNFTokenPageLinks, + noPriv, ({ {sfLedgerFixType, soeREQUIRED}, {sfOwner, soeOPTIONAL}, })) /** This transaction type creates a MPTokensIssuance instance */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttMPTOKEN_ISSUANCE_CREATE, 54, MPTokenIssuanceCreate, Delegation::delegatable, featureMPTokensV1, + createMPTIssuance, ({ {sfAssetScale, soeOPTIONAL}, {sfTransferFee, soeOPTIONAL}, {sfMaximumAmount, soeOPTIONAL}, {sfMPTokenMetadata, soeOPTIONAL}, {sfDomainID, soeOPTIONAL}, + {sfMutableFlags, soeOPTIONAL}, })) /** This transaction type destroys a MPTokensIssuance instance */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttMPTOKEN_ISSUANCE_DESTROY, 55, MPTokenIssuanceDestroy, Delegation::delegatable, featureMPTokensV1, + destroyMPTIssuance, ({ {sfMPTokenIssuanceID, soeREQUIRED}, })) /** This transaction type sets flags on a MPTokensIssuance or MPToken instance */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttMPTOKEN_ISSUANCE_SET, 56, MPTokenIssuanceSet, Delegation::delegatable, featureMPTokensV1, + noPriv, ({ {sfMPTokenIssuanceID, soeREQUIRED}, {sfHolder, soeOPTIONAL}, {sfDomainID, soeOPTIONAL}, + {sfMPTokenMetadata, soeOPTIONAL}, + {sfTransferFee, soeOPTIONAL}, + {sfMutableFlags, soeOPTIONAL}, })) /** This transaction type authorizes a MPToken instance */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttMPTOKEN_AUTHORIZE, 57, MPTokenAuthorize, Delegation::delegatable, featureMPTokensV1, + mustAuthorizeMPT, ({ {sfMPTokenIssuanceID, soeREQUIRED}, {sfHolder, soeOPTIONAL}, })) /** This transaction type create an Credential instance */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttCREDENTIAL_CREATE, 58, CredentialCreate, Delegation::delegatable, featureCredentials, + noPriv, ({ {sfSubject, soeREQUIRED}, {sfCredentialType, soeREQUIRED}, @@ -592,6 +775,7 @@ TRANSACTION(ttCREDENTIAL_CREATE, 58, CredentialCreate, TRANSACTION(ttCREDENTIAL_ACCEPT, 59, CredentialAccept, Delegation::delegatable, featureCredentials, + noPriv, ({ {sfIssuer, soeREQUIRED}, {sfCredentialType, soeREQUIRED}, @@ -601,6 +785,7 @@ TRANSACTION(ttCREDENTIAL_ACCEPT, 59, CredentialAccept, TRANSACTION(ttCREDENTIAL_DELETE, 60, CredentialDelete, Delegation::delegatable, featureCredentials, + noPriv, ({ {sfSubject, soeOPTIONAL}, {sfIssuer, soeOPTIONAL}, @@ -608,9 +793,13 @@ TRANSACTION(ttCREDENTIAL_DELETE, 60, CredentialDelete, })) /** This transaction type modify a NFToken */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttNFTOKEN_MODIFY, 61, NFTokenModify, Delegation::delegatable, featureDynamicNFT, + noPriv, ({ {sfNFTokenID, soeREQUIRED}, {sfOwner, soeOPTIONAL}, @@ -618,35 +807,51 @@ TRANSACTION(ttNFTOKEN_MODIFY, 61, NFTokenModify, })) /** This transaction type creates or modifies a Permissioned Domain */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttPERMISSIONED_DOMAIN_SET, 62, PermissionedDomainSet, Delegation::delegatable, featurePermissionedDomains, + noPriv, ({ {sfDomainID, soeOPTIONAL}, {sfAcceptedCredentials, soeREQUIRED}, })) /** This transaction type deletes a Permissioned Domain */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttPERMISSIONED_DOMAIN_DELETE, 63, PermissionedDomainDelete, Delegation::delegatable, featurePermissionedDomains, + noPriv, ({ {sfDomainID, soeREQUIRED}, })) /** This transaction type delegates authorized account specified permissions */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttDELEGATE_SET, 64, DelegateSet, Delegation::notDelegatable, featurePermissionDelegation, + noPriv, ({ {sfAuthorize, soeREQUIRED}, {sfPermissions, soeREQUIRED}, })) /** This transaction creates a single asset vault. */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttVAULT_CREATE, 65, VaultCreate, Delegation::delegatable, featureSingleAssetVault, + createPseudoAcct | createMPTIssuance, ({ {sfAsset, soeREQUIRED, soeMPTSupported}, {sfAssetsMaximum, soeOPTIONAL}, @@ -658,9 +863,13 @@ TRANSACTION(ttVAULT_CREATE, 65, VaultCreate, })) /** This transaction updates a single asset vault. */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttVAULT_SET, 66, VaultSet, Delegation::delegatable, featureSingleAssetVault, + noPriv, ({ {sfVaultID, soeREQUIRED}, {sfAssetsMaximum, soeOPTIONAL}, @@ -669,26 +878,38 @@ TRANSACTION(ttVAULT_SET, 66, VaultSet, })) /** This transaction deletes a single asset vault. */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttVAULT_DELETE, 67, VaultDelete, Delegation::delegatable, featureSingleAssetVault, + mustDeleteAcct | destroyMPTIssuance, ({ {sfVaultID, soeREQUIRED}, })) /** This transaction trades assets for shares with a vault. */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttVAULT_DEPOSIT, 68, VaultDeposit, Delegation::delegatable, featureSingleAssetVault, + mayAuthorizeMPT, ({ {sfVaultID, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported}, })) /** This transaction trades shares for assets with a vault. */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttVAULT_WITHDRAW, 69, VaultWithdraw, Delegation::delegatable, featureSingleAssetVault, + mayDeleteMPT, ({ {sfVaultID, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported}, @@ -697,9 +918,13 @@ TRANSACTION(ttVAULT_WITHDRAW, 69, VaultWithdraw, })) /** This transaction claws back tokens from a vault. */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttVAULT_CLAWBACK, 70, VaultClawback, Delegation::delegatable, featureSingleAssetVault, + mayDeleteMPT, ({ {sfVaultID, soeREQUIRED}, {sfHolder, soeREQUIRED}, @@ -707,9 +932,13 @@ TRANSACTION(ttVAULT_CLAWBACK, 70, VaultClawback, })) /** This transaction type batches together transactions. */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttBATCH, 71, Batch, Delegation::notDelegatable, featureBatch, + noPriv, ({ {sfRawTransactions, soeREQUIRED}, {sfBatchSigners, soeOPTIONAL}, @@ -732,9 +961,13 @@ TRANSACTION(ttBATCH, 71, Batch, For details, see: https://xrpl.org/amendments.html */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttAMENDMENT, 100, EnableAmendment, Delegation::notDelegatable, uint256{}, + noPriv, ({ {sfLedgerSequence, soeREQUIRED}, {sfAmendment, soeREQUIRED}, @@ -746,6 +979,7 @@ TRANSACTION(ttAMENDMENT, 100, EnableAmendment, TRANSACTION(ttFEE, 101, SetFee, Delegation::notDelegatable, uint256{}, + noPriv, ({ {sfLedgerSequence, soeOPTIONAL}, // Old version uses raw numbers @@ -766,6 +1000,7 @@ TRANSACTION(ttFEE, 101, SetFee, TRANSACTION(ttUNL_MODIFY, 102, UNLModify, Delegation::notDelegatable, uint256{}, + noPriv, ({ {sfUNLModifyDisabling, soeREQUIRED}, {sfLedgerSequence, soeREQUIRED}, diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index d847cf6012..8609aedaef 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -722,11 +722,11 @@ JSS(write_load); // out: GetCounts #pragma push_macro("LEDGER_ENTRY_DUPLICATE") #undef LEDGER_ENTRY_DUPLICATE -#define LEDGER_ENTRY(tag, value, name, rpcName, fields) \ - JSS(name); \ +#define LEDGER_ENTRY(tag, value, name, rpcName, ...) \ + JSS(name); \ JSS(rpcName); -#define LEDGER_ENTRY_DUPLICATE(tag, value, name, rpcName, fields) JSS(rpcName); +#define LEDGER_ENTRY_DUPLICATE(tag, value, name, rpcName, ...) JSS(rpcName); #include diff --git a/include/xrpl/server/detail/Door.h b/include/xrpl/server/detail/Door.h index 7906af2a52..65bc310d4c 100644 --- a/include/xrpl/server/detail/Door.h +++ b/include/xrpl/server/detail/Door.h @@ -30,15 +30,29 @@ #include #include #include +#include #include +#include #include #include #include #include +#include +#if !BOOST_OS_WINDOWS +#include + +#include +#include +#endif + +#include #include +#include #include #include +#include +#include namespace ripple { @@ -98,10 +112,27 @@ private: boost::asio::strand strand_; bool ssl_; bool plain_; + static constexpr std::chrono::milliseconds INITIAL_ACCEPT_DELAY{50}; + static constexpr std::chrono::milliseconds MAX_ACCEPT_DELAY{2000}; + std::chrono::milliseconds accept_delay_{INITIAL_ACCEPT_DELAY}; + boost::asio::steady_timer backoff_timer_; + static constexpr double FREE_FD_THRESHOLD = 0.70; + + struct FDStats + { + std::uint64_t used{0}; + std::uint64_t limit{0}; + }; void reOpen(); + std::optional + query_fd_stats() const; + + bool + should_throttle_for_fds(); + public: Door( Handler& handler, @@ -299,6 +330,7 @@ Door::Door( , plain_( port_.protocol.count("http") > 0 || port_.protocol.count("ws") > 0 || port_.protocol.count("ws2")) + , backoff_timer_(io_context) { reOpen(); } @@ -323,6 +355,7 @@ Door::close() return boost::asio::post( strand_, std::bind(&Door::close, this->shared_from_this())); + backoff_timer_.cancel(); error_code ec; acceptor_.close(ec); } @@ -368,6 +401,17 @@ Door::do_accept(boost::asio::yield_context do_yield) { while (acceptor_.is_open()) { + if (should_throttle_for_fds()) + { + backoff_timer_.expires_after(accept_delay_); + boost::system::error_code tec; + backoff_timer_.async_wait(do_yield[tec]); + accept_delay_ = std::min(accept_delay_ * 2, MAX_ACCEPT_DELAY); + JLOG(j_.warn()) << "Throttling do_accept for " + << accept_delay_.count() << "ms."; + continue; + } + error_code ec; endpoint_type remote_address; stream_type stream(ioc_); @@ -377,15 +421,28 @@ Door::do_accept(boost::asio::yield_context do_yield) { if (ec == boost::asio::error::operation_aborted) break; - JLOG(j_.error()) << "accept: " << ec.message(); - if (ec == boost::asio::error::no_descriptors) + + if (ec == boost::asio::error::no_descriptors || + ec == boost::asio::error::no_buffer_space) { - JLOG(j_.info()) << "re-opening acceptor"; - reOpen(); + JLOG(j_.warn()) << "accept: Too many open files. Pausing for " + << accept_delay_.count() << "ms."; + + backoff_timer_.expires_after(accept_delay_); + boost::system::error_code tec; + backoff_timer_.async_wait(do_yield[tec]); + + accept_delay_ = std::min(accept_delay_ * 2, MAX_ACCEPT_DELAY); + } + else + { + JLOG(j_.error()) << "accept error: " << ec.message(); } continue; } + accept_delay_ = INITIAL_ACCEPT_DELAY; + if (ssl_ && plain_) { if (auto sp = ios().template emplace( @@ -408,6 +465,60 @@ Door::do_accept(boost::asio::yield_context do_yield) } } +template +std::optional::FDStats> +Door::query_fd_stats() const +{ +#if BOOST_OS_WINDOWS + return std::nullopt; +#else + FDStats s; + struct rlimit rl; + if (getrlimit(RLIMIT_NOFILE, &rl) != 0 || rl.rlim_cur == RLIM_INFINITY) + return std::nullopt; + s.limit = static_cast(rl.rlim_cur); +#if BOOST_OS_LINUX + constexpr char const* kFdDir = "/proc/self/fd"; +#else + constexpr char const* kFdDir = "/dev/fd"; +#endif + if (DIR* d = ::opendir(kFdDir)) + { + std::uint64_t cnt = 0; + while (::readdir(d) != nullptr) + ++cnt; + ::closedir(d); + // readdir counts '.', '..', and the DIR* itself shows in the list + s.used = (cnt >= 3) ? (cnt - 3) : 0; + return s; + } + return std::nullopt; +#endif +} + +template +bool +Door::should_throttle_for_fds() +{ +#if BOOST_OS_WINDOWS + return false; +#else + auto const stats = query_fd_stats(); + if (!stats || stats->limit == 0) + return false; + + auto const& s = *stats; + auto const free = (s.limit > s.used) ? (s.limit - s.used) : 0ull; + double const free_ratio = + static_cast(free) / static_cast(s.limit); + if (free_ratio < FREE_FD_THRESHOLD) + { + return true; + } + return false; +#endif +} + } // namespace ripple #endif diff --git a/src/xrpld/ledger/detail/ApplyStateTable.cpp b/src/libxrpl/ledger/ApplyStateTable.cpp similarity index 99% rename from src/xrpld/ledger/detail/ApplyStateTable.cpp rename to src/libxrpl/ledger/ApplyStateTable.cpp index 2a740093d9..7b041939d4 100644 --- a/src/xrpld/ledger/detail/ApplyStateTable.cpp +++ b/src/libxrpl/ledger/ApplyStateTable.cpp @@ -17,11 +17,10 @@ */ //============================================================================== -#include - #include #include #include +#include #include #include diff --git a/src/xrpld/ledger/detail/ApplyView.cpp b/src/libxrpl/ledger/ApplyView.cpp similarity index 99% rename from src/xrpld/ledger/detail/ApplyView.cpp rename to src/libxrpl/ledger/ApplyView.cpp index 3191b47cbb..8a0fd51bd8 100644 --- a/src/xrpld/ledger/detail/ApplyView.cpp +++ b/src/libxrpl/ledger/ApplyView.cpp @@ -17,10 +17,9 @@ */ //============================================================================== -#include - #include #include +#include #include namespace ripple { diff --git a/src/xrpld/ledger/detail/ApplyViewBase.cpp b/src/libxrpl/ledger/ApplyViewBase.cpp similarity index 98% rename from src/xrpld/ledger/detail/ApplyViewBase.cpp rename to src/libxrpl/ledger/ApplyViewBase.cpp index 1d93eae7aa..dd0e043474 100644 --- a/src/xrpld/ledger/detail/ApplyViewBase.cpp +++ b/src/libxrpl/ledger/ApplyViewBase.cpp @@ -17,7 +17,7 @@ */ //============================================================================== -#include +#include namespace ripple { namespace detail { diff --git a/src/xrpld/ledger/detail/ApplyViewImpl.cpp b/src/libxrpl/ledger/ApplyViewImpl.cpp similarity index 97% rename from src/xrpld/ledger/detail/ApplyViewImpl.cpp rename to src/libxrpl/ledger/ApplyViewImpl.cpp index 3fd9478b54..9429bcee6e 100644 --- a/src/xrpld/ledger/detail/ApplyViewImpl.cpp +++ b/src/libxrpl/ledger/ApplyViewImpl.cpp @@ -17,7 +17,7 @@ */ //============================================================================== -#include +#include namespace ripple { diff --git a/src/xrpld/ledger/detail/BookDirs.cpp b/src/libxrpl/ledger/BookDirs.cpp similarity index 98% rename from src/xrpld/ledger/detail/BookDirs.cpp rename to src/libxrpl/ledger/BookDirs.cpp index 41a14945a6..f777d23aca 100644 --- a/src/xrpld/ledger/detail/BookDirs.cpp +++ b/src/libxrpl/ledger/BookDirs.cpp @@ -18,9 +18,8 @@ */ //============================================================================== -#include -#include - +#include +#include #include namespace ripple { diff --git a/src/xrpld/ledger/detail/CachedView.cpp b/src/libxrpl/ledger/CachedView.cpp similarity index 98% rename from src/xrpld/ledger/detail/CachedView.cpp rename to src/libxrpl/ledger/CachedView.cpp index 365d63e400..0bec9094d2 100644 --- a/src/xrpld/ledger/detail/CachedView.cpp +++ b/src/libxrpl/ledger/CachedView.cpp @@ -17,9 +17,8 @@ */ //============================================================================== -#include - #include +#include namespace ripple { namespace detail { diff --git a/src/xrpld/app/misc/CredentialHelpers.cpp b/src/libxrpl/ledger/CredentialHelpers.cpp similarity index 99% rename from src/xrpld/app/misc/CredentialHelpers.cpp rename to src/libxrpl/ledger/CredentialHelpers.cpp index 6d1f9f78c5..965d6f6911 100644 --- a/src/xrpld/app/misc/CredentialHelpers.cpp +++ b/src/libxrpl/ledger/CredentialHelpers.cpp @@ -17,9 +17,8 @@ */ //============================================================================== -#include -#include - +#include +#include #include #include diff --git a/src/xrpld/ledger/detail/Dir.cpp b/src/libxrpl/ledger/Dir.cpp similarity index 99% rename from src/xrpld/ledger/detail/Dir.cpp rename to src/libxrpl/ledger/Dir.cpp index 01d4487276..ea57369495 100644 --- a/src/xrpld/ledger/detail/Dir.cpp +++ b/src/libxrpl/ledger/Dir.cpp @@ -17,7 +17,7 @@ */ //============================================================================== -#include +#include namespace ripple { diff --git a/src/xrpld/ledger/detail/OpenView.cpp b/src/libxrpl/ledger/OpenView.cpp similarity index 99% rename from src/xrpld/ledger/detail/OpenView.cpp rename to src/libxrpl/ledger/OpenView.cpp index 73e502a5e2..c40434b38d 100644 --- a/src/xrpld/ledger/detail/OpenView.cpp +++ b/src/libxrpl/ledger/OpenView.cpp @@ -17,9 +17,8 @@ */ //============================================================================== -#include - #include +#include namespace ripple { diff --git a/src/xrpld/ledger/detail/PaymentSandbox.cpp b/src/libxrpl/ledger/PaymentSandbox.cpp similarity index 99% rename from src/xrpld/ledger/detail/PaymentSandbox.cpp rename to src/libxrpl/ledger/PaymentSandbox.cpp index 3eab845472..ba59b573fa 100644 --- a/src/xrpld/ledger/detail/PaymentSandbox.cpp +++ b/src/libxrpl/ledger/PaymentSandbox.cpp @@ -17,11 +17,9 @@ */ //============================================================================== -#include -#include -#include - #include +#include +#include #include namespace ripple { diff --git a/src/xrpld/ledger/detail/RawStateTable.cpp b/src/libxrpl/ledger/RawStateTable.cpp similarity index 99% rename from src/xrpld/ledger/detail/RawStateTable.cpp rename to src/libxrpl/ledger/RawStateTable.cpp index f19eed8297..e6b1467581 100644 --- a/src/xrpld/ledger/detail/RawStateTable.cpp +++ b/src/libxrpl/ledger/RawStateTable.cpp @@ -17,9 +17,8 @@ */ //============================================================================== -#include - #include +#include namespace ripple { namespace detail { diff --git a/src/xrpld/ledger/detail/ReadView.cpp b/src/libxrpl/ledger/ReadView.cpp similarity index 98% rename from src/xrpld/ledger/detail/ReadView.cpp rename to src/libxrpl/ledger/ReadView.cpp index 69a4b5d6a9..449a5a9cec 100644 --- a/src/xrpld/ledger/detail/ReadView.cpp +++ b/src/libxrpl/ledger/ReadView.cpp @@ -17,7 +17,7 @@ */ //============================================================================== -#include +#include namespace ripple { diff --git a/src/xrpld/ledger/detail/View.cpp b/src/libxrpl/ledger/View.cpp similarity index 95% rename from src/xrpld/ledger/detail/View.cpp rename to src/libxrpl/ledger/View.cpp index 708d5b29f7..10b7e81b4f 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -17,14 +17,13 @@ */ //============================================================================== -#include -#include -#include - #include #include #include #include +#include +#include +#include #include #include #include @@ -627,8 +626,8 @@ xrpLiquid( std::uint32_t const ownerCount = confineOwnerCount( view.ownerCountHook(id, sle->getFieldU32(sfOwnerCount)), ownerCountAdj); - // AMMs have no reserve requirement - auto const reserve = sle->isFieldPresent(sfAMMID) + // Pseudo-accounts have no reserve requirement + auto const reserve = isPseudoAccount(sle) ? XRPAmount{0} : view.fees().accountReserve(ownerCount); @@ -1040,7 +1039,7 @@ adjustOwnerCount( AccountID const id = (*sle)[sfAccount]; std::uint32_t const adjusted = confineOwnerCount(current, amount, id, j); view.adjustOwnerCountHook(id, current, adjusted); - sle->setFieldU32(sfOwnerCount, adjusted); + sle->at(sfOwnerCount) = adjusted; view.update(sle); } @@ -1080,15 +1079,51 @@ pseudoAccountAddress(ReadView const& view, uint256 const& pseudoOwnerKey) return beast::zero; } -// Note, the list of the pseudo-account designator fields below MUST be -// maintained but it does NOT need to be amendment-gated, since a -// non-active amendment will not set any field, by definition. Specific -// properties of a pseudo-account are NOT checked here, that's what +// Pseudo-account designator fields MUST be maintained by including the +// SField::sMD_PseudoAccount flag in the SField definition. (Don't forget to +// "| SField::sMD_Default"!) The fields do NOT need to be amendment-gated, +// since a non-active amendment will not set any field, by definition. +// Specific properties of a pseudo-account are NOT checked here, that's what // InvariantCheck is for. -static std::array const pseudoAccountOwnerFields = { - &sfAMMID, // - &sfVaultID, // -}; +[[nodiscard]] std::vector const& +getPseudoAccountFields() +{ + static std::vector const pseudoFields = []() { + auto const ar = LedgerFormats::getInstance().findByType(ltACCOUNT_ROOT); + if (!ar) + { + // LCOV_EXCL_START + LogicError( + "ripple::isPseudoAccount : unable to find account root ledger " + "format"); + // LCOV_EXCL_STOP + } + auto const& soTemplate = ar->getSOTemplate(); + + std::vector pseudoFields; + for (auto const& field : soTemplate) + { + if (field.sField().shouldMeta(SField::sMD_PseudoAccount)) + pseudoFields.emplace_back(&field.sField()); + } + return pseudoFields; + }(); + return pseudoFields; +} + +[[nodiscard]] bool +isPseudoAccount(std::shared_ptr sleAcct) +{ + auto const& fields = getPseudoAccountFields(); + + // Intentionally use defensive coding here because it's cheap and makes the + // semantics of true return value clean. + return sleAcct && sleAcct->getType() == ltACCOUNT_ROOT && + std::count_if( + fields.begin(), fields.end(), [&sleAcct](SField const* sf) -> bool { + return sleAcct->isFieldPresent(*sf); + }) > 0; +} Expected, TER> createPseudoAccount( @@ -1096,10 +1131,11 @@ createPseudoAccount( uint256 const& pseudoOwnerKey, SField const& ownerField) { + auto const& fields = getPseudoAccountFields(); XRPL_ASSERT( std::count_if( - pseudoAccountOwnerFields.begin(), - pseudoAccountOwnerFields.end(), + fields.begin(), + fields.end(), [&ownerField](SField const* sf) -> bool { return *sf == ownerField; }) == 1, @@ -1135,18 +1171,42 @@ createPseudoAccount( return account; } -[[nodiscard]] bool -isPseudoAccount(std::shared_ptr sleAcct) +[[nodiscard]] TER +canAddHolding(ReadView const& view, Issue const& issue) { - // Intentionally use defensive coding here because it's cheap and makes the - // semantics of true return value clean. - return sleAcct && sleAcct->getType() == ltACCOUNT_ROOT && - std::count_if( - pseudoAccountOwnerFields.begin(), - pseudoAccountOwnerFields.end(), - [&sleAcct](SField const* sf) -> bool { - return sleAcct->isFieldPresent(*sf); - }) > 0; + if (issue.native()) + return tesSUCCESS; // No special checks for XRP + + auto const issuer = view.read(keylet::account(issue.getIssuer())); + if (!issuer) + return terNO_ACCOUNT; + else if (!issuer->isFlag(lsfDefaultRipple)) + return terNO_RIPPLE; + + return tesSUCCESS; +} + +[[nodiscard]] TER +canAddHolding(ReadView const& view, MPTIssue const& mptIssue) +{ + auto mptID = mptIssue.getMptID(); + auto issuance = view.read(keylet::mptIssuance(mptID)); + if (!issuance) + return tecOBJECT_NOT_FOUND; + if (!issuance->isFlag(lsfMPTCanTransfer)) + return tecNO_AUTH; + + return tesSUCCESS; +} + +[[nodiscard]] TER +canAddHolding(ReadView const& view, Asset const& asset) +{ + return std::visit( + [&](TIss const& issue) -> TER { + return canAddHolding(view, issue); + }, + asset.value()); } [[nodiscard]] TER @@ -3006,11 +3066,17 @@ rippleUnlockEscrowMPT( ApplyView& view, AccountID const& sender, AccountID const& receiver, - STAmount const& amount, + STAmount const& netAmount, + STAmount const& grossAmount, beast::Journal j) { - auto const issuer = amount.getIssuer(); - auto const mptIssue = amount.get(); + if (!view.rules().enabled(fixTokenEscrowV1)) + XRPL_ASSERT( + netAmount == grossAmount, + "ripple::rippleUnlockEscrowMPT : netAmount == grossAmount"); + + auto const& issuer = netAmount.getIssuer(); + auto const& mptIssue = netAmount.get(); auto const mptID = keylet::mptIssuance(mptIssue.getMptID()); auto sleIssuance = view.peek(mptID); if (!sleIssuance) @@ -3031,7 +3097,7 @@ rippleUnlockEscrowMPT( } // LCOV_EXCL_STOP auto const locked = sleIssuance->getFieldU64(sfLockedAmount); - auto const redeem = amount.mpt().value(); + auto const redeem = grossAmount.mpt().value(); // Underflow check for subtraction if (!canSubtract( @@ -3060,11 +3126,11 @@ rippleUnlockEscrowMPT( { // LCOV_EXCL_START JLOG(j.error()) << "rippleUnlockEscrowMPT: MPToken not found for " << receiver; - return tecOBJECT_NOT_FOUND; // LCOV_EXCL_LINE + return tecOBJECT_NOT_FOUND; } // LCOV_EXCL_STOP auto current = sle->getFieldU64(sfMPTAmount); - auto delta = amount.mpt().value(); + auto delta = netAmount.mpt().value(); // Overflow check for addition if (!canAdd(STAmount(mptIssue, current), STAmount(mptIssue, delta))) @@ -3082,7 +3148,7 @@ rippleUnlockEscrowMPT( { // Decrease the Issuance OutstandingAmount auto const outstanding = sleIssuance->getFieldU64(sfOutstandingAmount); - auto const redeem = amount.mpt().value(); + auto const redeem = netAmount.mpt().value(); // Underflow check for subtraction if (!canSubtract( @@ -3126,7 +3192,7 @@ rippleUnlockEscrowMPT( } // LCOV_EXCL_STOP auto const locked = sle->getFieldU64(sfLockedAmount); - auto const delta = amount.mpt().value(); + auto const delta = grossAmount.mpt().value(); // Underflow check for subtraction if (!canSubtract(STAmount(mptIssue, locked), STAmount(mptIssue, delta))) @@ -3144,6 +3210,28 @@ rippleUnlockEscrowMPT( sle->setFieldU64(sfLockedAmount, newLocked); view.update(sle); } + + // Note: The gross amount is the amount that was locked, the net + // amount is the amount that is being unlocked. The difference is the fee + // that was charged for the transfer. If this difference is greater than + // zero, we need to update the outstanding amount. + auto const diff = grossAmount.mpt().value() - netAmount.mpt().value(); + if (diff != 0) + { + auto const outstanding = sleIssuance->getFieldU64(sfOutstandingAmount); + // Underflow check for subtraction + if (!canSubtract( + STAmount(mptIssue, outstanding), STAmount(mptIssue, diff))) + { // LCOV_EXCL_START + JLOG(j.error()) + << "rippleUnlockEscrowMPT: insufficient outstanding amount for " + << mptIssue.getMptID() << ": " << outstanding << " < " << diff; + return tecINTERNAL; + } // LCOV_EXCL_STOP + + sleIssuance->setFieldU64(sfOutstandingAmount, outstanding - diff); + view.update(sleIssuance); + } return tesSUCCESS; } diff --git a/src/libxrpl/net/HTTPClient.cpp b/src/libxrpl/net/HTTPClient.cpp index 964be32dd8..74b8b61ca6 100644 --- a/src/libxrpl/net/HTTPClient.cpp +++ b/src/libxrpl/net/HTTPClient.cpp @@ -383,7 +383,7 @@ public: static boost::regex reStatus{ "\\`HTTP/1\\S+ (\\d{3}) .*\\'"}; // HTTP/1.1 200 OK static boost::regex reSize{ - "\\`.*\\r\\nContent-Length:\\s+([0-9]+).*\\'"}; + "\\`.*\\r\\nContent-Length:\\s+([0-9]+).*\\'", boost::regex::icase}; static boost::regex reBody{"\\`.*\\r\\n\\r\\n(.*)\\'"}; boost::smatch smMatch; diff --git a/src/libxrpl/protocol/BuildInfo.cpp b/src/libxrpl/protocol/BuildInfo.cpp index d5077aa44d..b54c74c80d 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" +char const* const versionString = "2.6.1" // clang-format on #if defined(DEBUG) || defined(SANITIZER) diff --git a/src/libxrpl/protocol/Permissions.cpp b/src/libxrpl/protocol/Permissions.cpp index 781799f128..c9e32c5056 100644 --- a/src/libxrpl/protocol/Permissions.cpp +++ b/src/libxrpl/protocol/Permissions.cpp @@ -101,6 +101,22 @@ Permission::getInstance() return instance; } +std::optional +Permission::getPermissionName(std::uint32_t const value) const +{ + auto const permissionValue = static_cast(value); + if (auto const granular = getGranularName(permissionValue)) + return *granular; + + // not a granular permission, check if it maps to a transaction type + auto const txType = permissionToTxType(value); + if (auto const* item = TxFormats::getInstance().findByType(txType); + item != nullptr) + return item->getName(); + + return std::nullopt; +} + std::optional Permission::getGranularValue(std::string const& name) const { @@ -131,6 +147,19 @@ Permission::getGranularTxType(GranularPermissionType const& gpType) const return std::nullopt; } +std::optional> const +Permission::getTxFeature(TxType txType) const +{ + auto const txFeaturesIt = txFeatureMap_.find(txType); + XRPL_ASSERT( + txFeaturesIt != txFeatureMap_.end(), + "ripple::Permissions::getTxFeature : tx exists in txFeatureMap_"); + + if (txFeaturesIt->second == uint256{}) + return std::nullopt; + return txFeaturesIt->second; +} + bool Permission::isDelegatable( std::uint32_t const& permissionValue, @@ -150,16 +179,12 @@ Permission::isDelegatable( if (it == delegatableTx_.end()) return false; - auto const txFeaturesIt = txFeatureMap_.find(txType); - XRPL_ASSERT( - txFeaturesIt != txFeatureMap_.end(), - "ripple::Permissions::isDelegatable : tx exists in txFeatureMap_"); + auto const feature = getTxFeature(txType); // fixDelegateV1_1: Delegation is only allowed if the required amendment // for the transaction is enabled. For transactions that do not require // an amendment, delegation is always allowed. - if (txFeaturesIt->second != uint256{} && - !rules.enabled(txFeaturesIt->second)) + if (feature && !rules.enabled(*feature)) return false; } diff --git a/src/libxrpl/protocol/SField.cpp b/src/libxrpl/protocol/SField.cpp index 1ffce099b8..57e5849c00 100644 --- a/src/libxrpl/protocol/SField.cpp +++ b/src/libxrpl/protocol/SField.cpp @@ -17,6 +17,7 @@ */ //============================================================================== +#include #include #include @@ -27,7 +28,8 @@ namespace ripple { // Storage for static const members. SField::IsSigning const SField::notSigning; int SField::num = 0; -std::map SField::knownCodeToField; +std::unordered_map SField::knownCodeToField; +std::unordered_map SField::knownNameToField; // Give only this translation unit permission to construct SFields struct SField::private_access_tag_t @@ -45,7 +47,7 @@ TypedField::TypedField(private_access_tag_t pat, Args&&... args) } // Construct all compile-time SFields, and register them in the knownCodeToField -// database: +// and knownNameToField databases: // Use macros for most SField construction to enforce naming conventions. #pragma push_macro("UNTYPED_SFIELD") @@ -69,8 +71,8 @@ TypedField::TypedField(private_access_tag_t pat, Args&&... args) ##__VA_ARGS__); // SFields which, for historical reasons, do not follow naming conventions. -SField const sfInvalid(access, -1); -SField const sfGeneric(access, 0); +SField const sfInvalid(access, -1, ""); +SField const sfGeneric(access, 0, "Generic"); // The following two fields aren't used anywhere, but they break tests/have // downstream effects. SField const sfHash(access, STI_UINT256, 257, "hash"); @@ -99,19 +101,34 @@ SField::SField( , signingField(signing) , jsonName(fieldName.c_str()) { + XRPL_ASSERT( + !knownCodeToField.contains(fieldCode), + "ripple::SField::SField(tid,fv,fn,meta,signing) : fieldCode is unique"); + XRPL_ASSERT( + !knownNameToField.contains(fieldName), + "ripple::SField::SField(tid,fv,fn,meta,signing) : fieldName is unique"); knownCodeToField[fieldCode] = this; + knownNameToField[fieldName] = this; } -SField::SField(private_access_tag_t, int fc) +SField::SField(private_access_tag_t, int fc, char const* fn) : fieldCode(fc) , fieldType(STI_UNKNOWN) , fieldValue(0) + , fieldName(fn) , fieldMeta(sMD_Never) , fieldNum(++num) , signingField(IsSigning::yes) , jsonName(fieldName.c_str()) { + XRPL_ASSERT( + !knownCodeToField.contains(fieldCode), + "ripple::SField::SField(fc,fn) : fieldCode is unique"); + XRPL_ASSERT( + !knownNameToField.contains(fieldName), + "ripple::SField::SField(fc,fn) : fieldName is unique"); knownCodeToField[fieldCode] = this; + knownNameToField[fieldName] = this; } SField const& @@ -145,11 +162,11 @@ SField::compare(SField const& f1, SField const& f2) SField const& SField::getField(std::string const& fieldName) { - for (auto const& [_, f] : knownCodeToField) + auto it = knownNameToField.find(fieldName); + + if (it != knownNameToField.end()) { - (void)_; - if (f->fieldName == fieldName) - return *f; + return *(it->second); } return sfInvalid; } diff --git a/src/libxrpl/protocol/STInteger.cpp b/src/libxrpl/protocol/STInteger.cpp index a90e21491c..355fa4c113 100644 --- a/src/libxrpl/protocol/STInteger.cpp +++ b/src/libxrpl/protocol/STInteger.cpp @@ -62,8 +62,10 @@ STUInt8::getText() const if (transResultInfo(TER::fromInt(value_), token, human)) return human; + // LCOV_EXCL_START JLOG(debugLog().error()) << "Unknown result code in metadata: " << value_; + // LCOV_EXCL_STOP } return std::to_string(value_); @@ -80,8 +82,10 @@ STUInt8::getJson(JsonOptions) const if (transResultInfo(TER::fromInt(value_), token, human)) return token; + // LCOV_EXCL_START JLOG(debugLog().error()) << "Unknown result code in metadata: " << value_; + // LCOV_EXCL_STOP } return value_; @@ -171,6 +175,13 @@ template <> std::string STUInt32::getText() const { + if (getFName() == sfPermissionValue) + { + auto const permissionName = + Permission::getInstance().getPermissionName(value_); + if (permissionName) + return *permissionName; + } return std::to_string(value_); } @@ -180,23 +191,10 @@ STUInt32::getJson(JsonOptions) const { if (getFName() == sfPermissionValue) { - auto const permissionValue = - static_cast(value_); - auto const granular = - Permission::getInstance().getGranularName(permissionValue); - - if (granular) - { - return *granular; - } - else - { - auto const txType = - Permission::getInstance().permissionToTxType(value_); - auto item = TxFormats::getInstance().findByType(txType); - if (item != nullptr) - return item->getName(); - } + auto const permissionName = + Permission::getInstance().getPermissionName(value_); + if (permissionName) + return *permissionName; } return value_; @@ -251,4 +249,33 @@ STUInt64::getJson(JsonOptions) const return convertToString(value_, 16); // Convert to base 16 } +//------------------------------------------------------------------------------ + +template <> +STInteger::STInteger(SerialIter& sit, SField const& name) + : STInteger(name, sit.get32()) +{ +} + +template <> +SerializedTypeID +STInt32::getSType() const +{ + return STI_INT32; +} + +template <> +std::string +STInt32::getText() const +{ + return std::to_string(value_); +} + +template <> +Json::Value +STInt32::getJson(JsonOptions) const +{ + return value_; +} + } // namespace ripple diff --git a/src/libxrpl/protocol/STObject.cpp b/src/libxrpl/protocol/STObject.cpp index 9c23898a74..77e5fd1ad9 100644 --- a/src/libxrpl/protocol/STObject.cpp +++ b/src/libxrpl/protocol/STObject.cpp @@ -647,6 +647,12 @@ STObject::getFieldH256(SField const& field) const return getFieldByValue(field); } +std::int32_t +STObject::getFieldI32(SField const& field) const +{ + return getFieldByValue(field); +} + AccountID STObject::getAccountID(SField const& field) const { @@ -761,6 +767,12 @@ STObject::setFieldH256(SField const& field, uint256 const& v) setFieldUsingSetValue(field, v); } +void +STObject::setFieldI32(SField const& field, std::int32_t v) +{ + setFieldUsingSetValue(field, v); +} + void STObject::setFieldV256(SField const& field, STVector256 const& v) { diff --git a/src/libxrpl/protocol/STParsedJSON.cpp b/src/libxrpl/protocol/STParsedJSON.cpp index bc9aad0a13..f99fec6b87 100644 --- a/src/libxrpl/protocol/STParsedJSON.cpp +++ b/src/libxrpl/protocol/STParsedJSON.cpp @@ -83,7 +83,8 @@ constexpr std:: return static_cast(value); } -static std::string +// LCOV_EXCL_START +static inline std::string make_name(std::string const& object, std::string const& field) { if (field.empty()) @@ -92,7 +93,7 @@ make_name(std::string const& object, std::string const& field) return object + "." + field; } -static Json::Value +static inline Json::Value not_an_object(std::string const& object, std::string const& field) { return RPC::make_error( @@ -100,20 +101,20 @@ not_an_object(std::string const& object, std::string const& field) "Field '" + make_name(object, field) + "' is not a JSON object."); } -static Json::Value +static inline Json::Value not_an_object(std::string const& object) { return not_an_object(object, ""); } -static Json::Value +static inline Json::Value not_an_array(std::string const& object) { return RPC::make_error( rpcINVALID_PARAMS, "Field '" + object + "' is not a JSON array."); } -static Json::Value +static inline Json::Value unknown_field(std::string const& object, std::string const& field) { return RPC::make_error( @@ -121,7 +122,7 @@ unknown_field(std::string const& object, std::string const& field) "Field '" + make_name(object, field) + "' is unknown."); } -static Json::Value +static inline Json::Value out_of_range(std::string const& object, std::string const& field) { return RPC::make_error( @@ -129,7 +130,7 @@ out_of_range(std::string const& object, std::string const& field) "Field '" + make_name(object, field) + "' is out of range."); } -static Json::Value +static inline Json::Value bad_type(std::string const& object, std::string const& field) { return RPC::make_error( @@ -137,7 +138,7 @@ bad_type(std::string const& object, std::string const& field) "Field '" + make_name(object, field) + "' has bad type."); } -static Json::Value +static inline Json::Value invalid_data(std::string const& object, std::string const& field) { return RPC::make_error( @@ -145,13 +146,13 @@ invalid_data(std::string const& object, std::string const& field) "Field '" + make_name(object, field) + "' has invalid data."); } -static Json::Value +static inline Json::Value invalid_data(std::string const& object) { return invalid_data(object, ""); } -static Json::Value +static inline Json::Value array_expected(std::string const& object, std::string const& field) { return RPC::make_error( @@ -159,7 +160,7 @@ array_expected(std::string const& object, std::string const& field) "Field '" + make_name(object, field) + "' must be a JSON array."); } -static Json::Value +static inline Json::Value string_expected(std::string const& object, std::string const& field) { return RPC::make_error( @@ -167,7 +168,7 @@ string_expected(std::string const& object, std::string const& field) "Field '" + make_name(object, field) + "' must be a string."); } -static Json::Value +static inline Json::Value too_deep(std::string const& object) { return RPC::make_error( @@ -175,7 +176,7 @@ too_deep(std::string const& object) "Field '" + object + "' exceeds nesting depth limit."); } -static Json::Value +static inline Json::Value singleton_expected(std::string const& object, unsigned int index) { return RPC::make_error( @@ -184,7 +185,7 @@ singleton_expected(std::string const& object, unsigned int index) "]' must be an object with a single key/object value."); } -static Json::Value +static inline Json::Value template_mismatch(SField const& sField) { return RPC::make_error( @@ -193,7 +194,7 @@ template_mismatch(SField const& sField) "' contents did not meet requirements for that type."); } -static Json::Value +static inline Json::Value non_object_in_array(std::string const& item, Json::UInt index) { return RPC::make_error( @@ -201,6 +202,176 @@ non_object_in_array(std::string const& item, Json::UInt index) "Item '" + item + "' at index " + std::to_string(index) + " is not an object. Arrays may only contain objects."); } +// LCOV_EXCL_STOP + +template +static std::optional +parseUnsigned( + SField const& field, + std::string const& json_name, + std::string const& fieldName, + SField const* name, + Json::Value const& value, + Json::Value& error) +{ + std::optional ret; + + try + { + if (value.isString()) + { + ret = detail::make_stvar( + field, + safe_cast( + beast::lexicalCastThrow(value.asString()))); + } + else if (value.isInt()) + { + ret = detail::make_stvar( + field, + to_unsigned(value.asInt())); + } + else if (value.isUInt()) + { + ret = detail::make_stvar( + field, + to_unsigned(value.asUInt())); + } + else + { + error = bad_type(json_name, fieldName); + return ret; + } + } + catch (std::exception const&) + { + error = invalid_data(json_name, fieldName); + return ret; + } + + return ret; +} + +template +static std::optional +parseUint16( + SField const& field, + std::string const& json_name, + std::string const& fieldName, + SField const* name, + Json::Value const& value, + Json::Value& error) +{ + std::optional ret; + + try + { + if (value.isString()) + { + std::string const strValue = value.asString(); + + if (!strValue.empty() && + ((strValue[0] < '0') || (strValue[0] > '9'))) + { + if (field == sfTransactionType) + { + ret = detail::make_stvar( + field, + safe_cast( + static_cast( + TxFormats::getInstance().findTypeByName( + strValue)))); + + if (*name == sfGeneric) + name = &sfTransaction; + } + else if (field == sfLedgerEntryType) + { + ret = detail::make_stvar( + field, + safe_cast( + static_cast( + LedgerFormats::getInstance().findTypeByName( + strValue)))); + + if (*name == sfGeneric) + name = &sfLedgerEntry; + } + else + { + error = invalid_data(json_name, fieldName); + return ret; + } + } + } + if (!ret) + return parseUnsigned( + field, json_name, fieldName, name, value, error); + } + catch (std::exception const&) + { + error = invalid_data(json_name, fieldName); + return ret; + } + + return ret; +} + +template +static std::optional +parseUint32( + SField const& field, + std::string const& json_name, + std::string const& fieldName, + SField const* name, + Json::Value const& value, + Json::Value& error) +{ + std::optional ret; + + try + { + if (value.isString()) + { + if (field == sfPermissionValue) + { + std::string const strValue = value.asString(); + auto const granularPermission = + Permission::getInstance().getGranularValue(strValue); + if (granularPermission) + { + ret = detail::make_stvar( + field, *granularPermission); + } + else + { + auto const& txType = + TxFormats::getInstance().findTypeByName(strValue); + ret = detail::make_stvar( + field, + Permission::getInstance().txToPermissionType(txType)); + } + } + else + { + ret = detail::make_stvar( + field, + safe_cast( + beast::lexicalCastThrow(value.asString()))); + } + } + if (!ret) + return parseUnsigned( + field, json_name, fieldName, name, value, error); + } + catch (std::exception const&) + { + error = invalid_data(json_name, fieldName); + return ret; + } + + return ret; +} // This function is used by parseObject to parse any JSON type that doesn't // recurse. Everything represented here is a leaf-type. @@ -216,10 +387,13 @@ parseLeaf( auto const& field = SField::getField(fieldName); + // checked in parseObject if (field == sfInvalid) { + // LCOV_EXCL_START error = unknown_field(json_name, fieldName); return ret; + // LCOV_EXCL_STOP } switch (field.fieldType) @@ -302,130 +476,18 @@ parseLeaf( break; case STI_UINT16: - try - { - if (value.isString()) - { - std::string const strValue = value.asString(); - - if (!strValue.empty() && - ((strValue[0] < '0') || (strValue[0] > '9'))) - { - if (field == sfTransactionType) - { - ret = detail::make_stvar( - field, - static_cast( - TxFormats::getInstance().findTypeByName( - strValue))); - - if (*name == sfGeneric) - name = &sfTransaction; - } - else if (field == sfLedgerEntryType) - { - ret = detail::make_stvar( - field, - static_cast( - LedgerFormats::getInstance().findTypeByName( - strValue))); - - if (*name == sfGeneric) - name = &sfLedgerEntry; - } - else - { - error = invalid_data(json_name, fieldName); - return ret; - } - } - else - { - ret = detail::make_stvar( - field, - beast::lexicalCastThrow(strValue)); - } - } - else if (value.isInt()) - { - ret = detail::make_stvar( - field, to_unsigned(value.asInt())); - } - else if (value.isUInt()) - { - ret = detail::make_stvar( - field, to_unsigned(value.asUInt())); - } - else - { - error = bad_type(json_name, fieldName); - return ret; - } - } - catch (std::exception const&) - { - error = invalid_data(json_name, fieldName); + ret = parseUint16( + field, json_name, fieldName, name, value, error); + if (!ret) return ret; - } break; case STI_UINT32: - try - { - if (value.isString()) - { - if (field == sfPermissionValue) - { - std::string const strValue = value.asString(); - auto const granularPermission = - Permission::getInstance().getGranularValue( - strValue); - if (granularPermission) - { - ret = detail::make_stvar( - field, *granularPermission); - } - else - { - auto const& txType = - TxFormats::getInstance().findTypeByName( - strValue); - ret = detail::make_stvar( - field, - Permission::getInstance().txToPermissionType( - txType)); - } - } - else - { - ret = detail::make_stvar( - field, - beast::lexicalCastThrow( - value.asString())); - } - } - else if (value.isInt()) - { - ret = detail::make_stvar( - field, to_unsigned(value.asInt())); - } - else if (value.isUInt()) - { - ret = detail::make_stvar( - field, safe_cast(value.asUInt())); - } - else - { - error = bad_type(json_name, fieldName); - return ret; - } - } - catch (std::exception const&) - { - error = invalid_data(json_name, fieldName); + ret = parseUint32( + field, json_name, fieldName, name, value, error); + if (!ret) return ret; - } break; @@ -501,30 +563,6 @@ parseLeaf( break; } - case STI_UINT192: { - if (!value.isString()) - { - error = bad_type(json_name, fieldName); - return ret; - } - - uint192 num; - - if (auto const s = value.asString(); !num.parseHex(s)) - { - if (!s.empty()) - { - error = invalid_data(json_name, fieldName); - return ret; - } - - num.zero(); - } - - ret = detail::make_stvar(field, num); - break; - } - case STI_UINT160: { if (!value.isString()) { @@ -549,6 +587,30 @@ parseLeaf( break; } + case STI_UINT192: { + if (!value.isString()) + { + error = bad_type(json_name, fieldName); + return ret; + } + + uint192 num; + + if (auto const s = value.asString(); !num.parseHex(s)) + { + if (!s.empty()) + { + error = invalid_data(json_name, fieldName); + return ret; + } + + num.zero(); + } + + ret = detail::make_stvar(field, num); + break; + } + case STI_UINT256: { if (!value.isString()) { @@ -573,6 +635,52 @@ parseLeaf( break; } + case STI_INT32: + try + { + if (value.isString()) + { + ret = detail::make_stvar( + field, + beast::lexicalCastThrow( + value.asString())); + } + else if (value.isInt()) + { + // future-proofing - a static assert failure if the JSON + // library ever supports larger ints + // In such case, we will need additional bounds checks here + static_assert( + std::is_same_v); + ret = detail::make_stvar(field, value.asInt()); + } + else if (value.isUInt()) + { + auto const uintValue = value.asUInt(); + if (uintValue > + static_cast( + std::numeric_limits::max())) + { + error = out_of_range(json_name, fieldName); + return ret; + } + ret = detail::make_stvar( + field, static_cast(uintValue)); + } + else + { + error = bad_type(json_name, fieldName); + return ret; + } + } + catch (std::exception const&) + { + error = invalid_data(json_name, fieldName); + return ret; + } + + break; + case STI_VL: if (!value.isString()) { @@ -703,6 +811,12 @@ parseLeaf( AccountID uAccount, uIssuer; Currency uCurrency; + if (!account && !currency && !issuer) + { + error = invalid_data(element_name); + return ret; + } + if (account) { // human account id @@ -1052,8 +1166,7 @@ parseArray( Json::Value const objectFields(json[i][objectName]); std::stringstream ss; - ss << json_name << "." - << "[" << i << "]." << objectName; + ss << json_name << "." << "[" << i << "]." << objectName; auto ret = parseObject( ss.str(), objectFields, nameField, depth + 1, error); @@ -1096,24 +1209,4 @@ STParsedJSONObject::STParsedJSONObject( object = parseObject(name, json, sfGeneric, 0, error); } -//------------------------------------------------------------------------------ - -STParsedJSONArray::STParsedJSONArray( - std::string const& name, - Json::Value const& json) -{ - using namespace STParsedJSONDetail; - auto arr = parseArray(name, json, sfGeneric, 0, error); - if (!arr) - array.reset(); - else - { - auto p = dynamic_cast(&arr->get()); - if (p == nullptr) - array.reset(); - else - array = std::move(*p); - } -} - } // namespace ripple diff --git a/src/libxrpl/protocol/STVar.cpp b/src/libxrpl/protocol/STVar.cpp index 24954c4add..c46effb47e 100644 --- a/src/libxrpl/protocol/STVar.cpp +++ b/src/libxrpl/protocol/STVar.cpp @@ -208,6 +208,9 @@ STVar::constructST(SerializedTypeID id, int depth, Args&&... args) case STI_UINT256: construct(std::forward(args)...); return; + case STI_INT32: + construct(std::forward(args)...); + return; case STI_VECTOR256: construct(std::forward(args)...); return; diff --git a/src/libxrpl/protocol/Serializer.cpp b/src/libxrpl/protocol/Serializer.cpp index b8a68d28b8..098e68d2b2 100644 --- a/src/libxrpl/protocol/Serializer.cpp +++ b/src/libxrpl/protocol/Serializer.cpp @@ -83,6 +83,12 @@ Serializer::addInteger(std::uint64_t i) { return add64(i); } +template <> +int +Serializer::addInteger(std::int32_t i) +{ + return add32(i); +} int Serializer::addRaw(Blob const& vector) diff --git a/src/libxrpl/protocol/TxFormats.cpp b/src/libxrpl/protocol/TxFormats.cpp index c10c023ee9..1966c29b51 100644 --- a/src/libxrpl/protocol/TxFormats.cpp +++ b/src/libxrpl/protocol/TxFormats.cpp @@ -55,7 +55,8 @@ TxFormats::TxFormats() #undef TRANSACTION #define UNWRAP(...) __VA_ARGS__ -#define TRANSACTION(tag, value, name, delegatable, amendment, fields) \ +#define TRANSACTION( \ + tag, value, name, delegatable, amendment, privileges, fields) \ add(jss::name, tag, UNWRAP fields, commonFields); #include diff --git a/src/test/app/AMMCalc_test.cpp b/src/test/app/AMMCalc_test.cpp index bebf2844b6..7349b38766 100644 --- a/src/test/app/AMMCalc_test.cpp +++ b/src/test/app/AMMCalc_test.cpp @@ -67,7 +67,7 @@ class AMMCalc_test : public beast::unit_test::suite // drops else if (match[1] == "XRPA") return XRPAmount{std::stoll(match[2])}; - return amountFromString(gw[match[1]], match[2]); + return amountFromString(gw[match[1]].asset(), match[2]); } return std::nullopt; } diff --git a/src/test/app/AMMExtended_test.cpp b/src/test/app/AMMExtended_test.cpp index 893e9e4f75..dcbebd2dc3 100644 --- a/src/test/app/AMMExtended_test.cpp +++ b/src/test/app/AMMExtended_test.cpp @@ -29,8 +29,8 @@ #include #include #include -#include +#include #include #include @@ -181,9 +181,9 @@ private: BEAST_EXPECT(expectLedgerEntryRoot( env, alice, XRP(20'000) - XRP(50) - txfee(env, 1))); - BEAST_EXPECT(expectLine(env, bob, USD1(100))); - BEAST_EXPECT(expectLine(env, bob, USD2(0))); - BEAST_EXPECT(expectLine(env, carol, USD2(50))); + BEAST_EXPECT(expectHolding(env, bob, USD1(100))); + BEAST_EXPECT(expectHolding(env, bob, USD2(0))); + BEAST_EXPECT(expectHolding(env, carol, USD2(50))); } } @@ -220,7 +220,7 @@ private: BEAST_EXPECT(expectLedgerEntryRoot( env, carol, XRP(30'000) - (txfee(env, 1)))); BEAST_EXPECT(expectOffers(env, carol, 0)); - BEAST_EXPECT(expectLine(env, carol, USD(30'000))); + BEAST_EXPECT(expectHolding(env, carol, USD(30'000))); // Order that can be filled env(offer(carol, XRP(100), USD(100)), @@ -230,7 +230,7 @@ private: XRP(10'000), USD(10'100), ammAlice.tokens())); BEAST_EXPECT(expectLedgerEntryRoot( env, carol, XRP(30'000) + XRP(100) - txfee(env, 2))); - BEAST_EXPECT(expectLine(env, carol, USD(29'900))); + BEAST_EXPECT(expectHolding(env, carol, USD(29'900))); BEAST_EXPECT(expectOffers(env, carol, 0)); }, {{XRP(10'100), USD(10'000)}}, @@ -254,7 +254,7 @@ private: BEAST_EXPECT(expectLedgerEntryRoot( env, carol, XRP(30'000) + XRP(100) - txfee(env, 1))); // AMM - BEAST_EXPECT(expectLine(env, carol, USD(29'900))); + BEAST_EXPECT(expectHolding(env, carol, USD(29'900))); BEAST_EXPECT(expectOffers(env, carol, 0)); }, {{XRP(10'100), USD(10'000)}}, @@ -327,7 +327,7 @@ private: USD(49), IOUAmount{273'861'278752583, -8})); - BEAST_EXPECT(expectLine(env, bob, STAmount{USD, 101})); + BEAST_EXPECT(expectHolding(env, bob, STAmount{USD, 101})); BEAST_EXPECT(expectLedgerEntryRoot( env, bob, XRP(300'000) - xrpTransferred - txfee(env, 1))); BEAST_EXPECT(expectOffers(env, bob, 0)); @@ -390,7 +390,7 @@ private: BEAST_EXPECT( ammBob.expectBalances(USD(300), XRP(1'000), ammBob.tokens())); - BEAST_EXPECT(expectLine(env, alice, USD(0))); + BEAST_EXPECT(expectHolding(env, alice, USD(0))); auto jrr = ledgerEntryRoot(env, alice); BEAST_EXPECT( @@ -423,7 +423,7 @@ private: BEAST_EXPECT(ammAlice.expectBalances( XRPAmount{9'900'990'100}, USD(10'100), ammAlice.tokens())); // initial 30,000 - 10,000AMM - 100pay - BEAST_EXPECT(expectLine(env, alice, USD(19'900))); + BEAST_EXPECT(expectHolding(env, alice, USD(19'900))); // initial 30,000 - 10,0000AMM + 99.009900pay - fee*3 BEAST_EXPECT(expectLedgerEntryRoot( env, @@ -453,7 +453,7 @@ private: env(pay(alice, bob, USD(100)), sendmax(XRP(100))); BEAST_EXPECT(ammAlice.expectBalances( XRP(10'100), USD(10'000), ammAlice.tokens())); - BEAST_EXPECT(expectLine(env, bob, USD(100))); + BEAST_EXPECT(expectHolding(env, bob, USD(100))); }, {{XRP(10'000), USD(10'100)}}, 0, @@ -533,7 +533,7 @@ private: STAmount{USD1, UINT64_C(5'030'181086519115), -12}, ammCarol.tokens())); BEAST_EXPECT(expectOffers(env, dan, 1, {{Amounts{XRP(200), EUR(20)}}})); - BEAST_EXPECT(expectLine(env, bob, STAmount{EUR1, 30})); + BEAST_EXPECT(expectHolding(env, bob, STAmount{EUR1, 30})); } void @@ -642,7 +642,7 @@ private: BEAST_EXPECT(ammAlice.expectBalances( XRP(10'000), USD(9'999), ammAlice.tokens())); BEAST_EXPECT(expectOffers(env, carol, 0)); - BEAST_EXPECT(expectLine(env, carol, USD(30'101))); + BEAST_EXPECT(expectHolding(env, carol, USD(30'101))); BEAST_EXPECT(expectLedgerEntryRoot( env, carol, XRP(30'000) - XRP(100) - txfee(env, 1))); }, @@ -682,7 +682,7 @@ private: env(offer(alice, USD(100), XRP(200)), json(jss::Flags, tfSell)); BEAST_EXPECT( ammBob.expectBalances(XRP(1'100), USD(2'000), ammBob.tokens())); - BEAST_EXPECT(expectLine(env, alice, USD(200))); + BEAST_EXPECT(expectHolding(env, alice, USD(200))); BEAST_EXPECT(expectLedgerEntryRoot(env, alice, XRP(250))); BEAST_EXPECT(expectOffers(env, alice, 0)); } @@ -733,7 +733,7 @@ private: STAmount(XTS, UINT64_C(101'010101010101), -12), XXX(99), ammAlice.tokens())); - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, bob, STAmount{XTS, UINT64_C(98'989898989899), -12})); } else @@ -742,10 +742,10 @@ private: STAmount(XTS, UINT64_C(101'0101010101011), -13), XXX(99), ammAlice.tokens())); - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, bob, STAmount{XTS, UINT64_C(98'9898989898989), -13})); } - BEAST_EXPECT(expectLine(env, bob, XXX(101))); + BEAST_EXPECT(expectHolding(env, bob, XXX(101))); } void @@ -783,8 +783,8 @@ private: XRP(10'100), USD(10'000), ammAlice.tokens())); BEAST_EXPECT(ammBob.expectBalances( XRP(10'000), EUR(10'100), ammBob.tokens())); - BEAST_EXPECT(expectLine(env, carol, USD(15'100))); - BEAST_EXPECT(expectLine(env, carol, EUR(14'900))); + BEAST_EXPECT(expectHolding(env, carol, USD(15'100))); + BEAST_EXPECT(expectHolding(env, carol, EUR(14'900))); BEAST_EXPECT(expectOffers(env, carol, 0)); } @@ -816,8 +816,8 @@ private: BEAST_EXPECT(ammAlice.expectBalances( XRP(10'100), USD(10'000), ammAlice.tokens())); - BEAST_EXPECT(expectLine(env, carol, USD(15'100))); - BEAST_EXPECT(expectLine(env, carol, EUR(14'900))); + BEAST_EXPECT(expectHolding(env, carol, USD(15'100))); + BEAST_EXPECT(expectHolding(env, carol, EUR(14'900))); BEAST_EXPECT(expectOffers(env, carol, 0)); BEAST_EXPECT(expectOffers(env, bob, 0)); } @@ -850,8 +850,8 @@ private: BEAST_EXPECT(ammBob.expectBalances( XRP(10'000), EUR(10'100), ammBob.tokens())); - BEAST_EXPECT(expectLine(env, carol, USD(15'100))); - BEAST_EXPECT(expectLine(env, carol, EUR(14'900))); + BEAST_EXPECT(expectHolding(env, carol, USD(15'100))); + BEAST_EXPECT(expectHolding(env, carol, EUR(14'900))); BEAST_EXPECT(expectOffers(env, carol, 0)); BEAST_EXPECT(expectOffers(env, alice, 0)); } @@ -894,7 +894,7 @@ private: XRP(20'220), STAmount{USD, UINT64_C(197'8239366963403), -13}, ammBob.tokens())); - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, alice, STAmount{USD, UINT64_C(1'002'17606330366), -11})); BEAST_EXPECT(expectOffers(env, alice, 0)); } @@ -912,7 +912,7 @@ private: XRP(21'500), STAmount{USD, UINT64_C(186'046511627907), -12}, ammBob.tokens())); - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, alice, STAmount{USD, UINT64_C(1'013'953488372093), -12})); BEAST_EXPECT(expectOffers(env, alice, 0)); } @@ -953,7 +953,7 @@ private: // AMM doesn't pay the transfer fee BEAST_EXPECT(ammAlice.expectBalances( XRP(10'100), USD(10'000), ammAlice.tokens())); - BEAST_EXPECT(expectLine(env, carol, USD(30'100))); + BEAST_EXPECT(expectHolding(env, carol, USD(30'100))); BEAST_EXPECT(expectOffers(env, carol, 0)); }, {{XRP(10'000), USD(10'100)}}, @@ -974,7 +974,7 @@ private: BEAST_EXPECT(ammAlice.expectBalances( XRP(10'000), USD(10'100), ammAlice.tokens())); // Carol pays 25% transfer fee - BEAST_EXPECT(expectLine(env, carol, USD(29'875))); + BEAST_EXPECT(expectHolding(env, carol, USD(29'875))); BEAST_EXPECT(expectOffers(env, carol, 0)); }, {{XRP(10'100), USD(10'000)}}, @@ -1011,9 +1011,9 @@ private: // AMM doesn't pay the transfer fee BEAST_EXPECT(ammAlice.expectBalances( XRP(10'100), USD(10'000), ammAlice.tokens())); - BEAST_EXPECT(expectLine(env, carol, USD(15'100))); + BEAST_EXPECT(expectHolding(env, carol, USD(15'100))); // Carol pays 25% transfer fee. - BEAST_EXPECT(expectLine(env, carol, EUR(14'875))); + BEAST_EXPECT(expectHolding(env, carol, EUR(14'875))); BEAST_EXPECT(expectOffers(env, carol, 0)); BEAST_EXPECT(expectOffers(env, bob, 0)); } @@ -1051,9 +1051,9 @@ private: // AMM doesn't pay the transfer fee BEAST_EXPECT(ammAlice.expectBalances( XRP(10'050), USD(10'000), ammAlice.tokens())); - BEAST_EXPECT(expectLine(env, carol, USD(15'050))); + BEAST_EXPECT(expectHolding(env, carol, USD(15'050))); // Carol pays 25% transfer fee. - BEAST_EXPECT(expectLine(env, carol, EUR(14'937.5))); + BEAST_EXPECT(expectHolding(env, carol, EUR(14'937.5))); BEAST_EXPECT(expectOffers(env, carol, 0)); BEAST_EXPECT( expectOffers(env, bob, 1, {{Amounts{EUR(50), XRP(50)}}})); @@ -1077,7 +1077,7 @@ private: env(pay(gw, carol, EUR(1'000)), sendmax(EUR(10'000))); env.close(); // 1000 / 0.8 - BEAST_EXPECT(expectLine(env, carol, EUR(1'250))); + BEAST_EXPECT(expectHolding(env, carol, EUR(1'250))); // The scenario: // o USD/XRP AMM is created. // o EUR/XRP Offer is created. @@ -1096,9 +1096,9 @@ private: // AMM doesn't pay the transfer fee BEAST_EXPECT(ammAlice.expectBalances( XRP(10'100), USD(10'000), ammAlice.tokens())); - BEAST_EXPECT(expectLine(env, carol, USD(100))); + BEAST_EXPECT(expectHolding(env, carol, USD(100))); // Carol pays 25% transfer fee: 1250 - 100(offer) - 25(transfer fee) - BEAST_EXPECT(expectLine(env, carol, EUR(1'125))); + BEAST_EXPECT(expectHolding(env, carol, EUR(1'125))); BEAST_EXPECT(expectOffers(env, carol, 0)); BEAST_EXPECT(expectOffers(env, bob, 0)); } @@ -1120,7 +1120,7 @@ private: env(pay(gw, alice, USD(11'000))); env(pay(gw, carol, EUR(1'000)), sendmax(EUR(10'000))); env.close(); - BEAST_EXPECT(expectLine(env, carol, EUR(1'000))); + BEAST_EXPECT(expectHolding(env, carol, EUR(1'000))); // The scenario: // o USD/XRP AMM is created. // o EUR/XRP Offer is created. @@ -1139,9 +1139,9 @@ private: // AMM pay doesn't transfer fee BEAST_EXPECT(ammAlice.expectBalances( XRP(10'100), USD(10'000), ammAlice.tokens())); - BEAST_EXPECT(expectLine(env, carol, USD(100))); + BEAST_EXPECT(expectHolding(env, carol, USD(100))); // Carol pays 25% transfer fee: 1000 - 100(offer) - 25(transfer fee) - BEAST_EXPECT(expectLine(env, carol, EUR(875))); + BEAST_EXPECT(expectHolding(env, carol, EUR(875))); BEAST_EXPECT(expectOffers(env, carol, 0)); BEAST_EXPECT(expectOffers(env, bob, 0)); } @@ -1170,7 +1170,7 @@ private: BEAST_EXPECT(ammBob.expectBalances( XRP(10'100), USD_bob(10'000), ammBob.tokens())); BEAST_EXPECT(expectOffers(env, alice, 0)); - BEAST_EXPECT(expectLine(env, alice, USD_bob(100))); + BEAST_EXPECT(expectHolding(env, alice, USD_bob(100))); } void @@ -1206,19 +1206,19 @@ private: env.close(); env(pay(dan, bob, D_BUX(100))); env.close(); - BEAST_EXPECT(expectLine(env, bob, D_BUX(100))); + BEAST_EXPECT(expectHolding(env, bob, D_BUX(100))); env(pay(ann, cam, D_BUX(60)), path(bob, dan), sendmax(A_BUX(200))); env.close(); - BEAST_EXPECT(expectLine(env, ann, A_BUX(none))); - BEAST_EXPECT(expectLine(env, ann, D_BUX(none))); - BEAST_EXPECT(expectLine(env, bob, A_BUX(72))); - BEAST_EXPECT(expectLine(env, bob, D_BUX(40))); - BEAST_EXPECT(expectLine(env, cam, A_BUX(none))); - BEAST_EXPECT(expectLine(env, cam, D_BUX(60))); - BEAST_EXPECT(expectLine(env, dan, A_BUX(none))); - BEAST_EXPECT(expectLine(env, dan, D_BUX(none))); + BEAST_EXPECT(expectHolding(env, ann, A_BUX(none))); + BEAST_EXPECT(expectHolding(env, ann, D_BUX(none))); + BEAST_EXPECT(expectHolding(env, bob, A_BUX(72))); + BEAST_EXPECT(expectHolding(env, bob, D_BUX(40))); + BEAST_EXPECT(expectHolding(env, cam, A_BUX(none))); + BEAST_EXPECT(expectHolding(env, cam, D_BUX(60))); + BEAST_EXPECT(expectHolding(env, dan, A_BUX(none))); + BEAST_EXPECT(expectHolding(env, dan, D_BUX(none))); AMM ammBob(env, bob, A_BUX(30), D_BUX(30)); @@ -1234,12 +1234,12 @@ private: BEAST_EXPECT( ammBob.expectBalances(A_BUX(30), D_BUX(30), ammBob.tokens())); - BEAST_EXPECT(expectLine(env, ann, A_BUX(none))); - BEAST_EXPECT(expectLine(env, ann, D_BUX(0))); - BEAST_EXPECT(expectLine(env, cam, A_BUX(none))); - BEAST_EXPECT(expectLine(env, cam, D_BUX(60))); - BEAST_EXPECT(expectLine(env, dan, A_BUX(0))); - BEAST_EXPECT(expectLine(env, dan, D_BUX(none))); + BEAST_EXPECT(expectHolding(env, ann, A_BUX(none))); + BEAST_EXPECT(expectHolding(env, ann, D_BUX(0))); + BEAST_EXPECT(expectHolding(env, cam, A_BUX(none))); + BEAST_EXPECT(expectHolding(env, cam, D_BUX(60))); + BEAST_EXPECT(expectHolding(env, dan, A_BUX(0))); + BEAST_EXPECT(expectHolding(env, dan, D_BUX(none))); } } @@ -1363,7 +1363,7 @@ private: env(pay(gw, bob, USD(50))); env.close(); - BEAST_EXPECT(expectLine(env, bob, USD(50))); + BEAST_EXPECT(expectHolding(env, bob, USD(50))); // Bob's offer should cross Alice's AMM env(offer(bob, XRP(50), USD(50))); @@ -1372,7 +1372,7 @@ private: BEAST_EXPECT( ammAlice.expectBalances(USD(1'050), XRP(1'000), ammAlice.tokens())); BEAST_EXPECT(expectOffers(env, bob, 0)); - BEAST_EXPECT(expectLine(env, bob, USD(0))); + BEAST_EXPECT(expectHolding(env, bob, USD(0))); } void @@ -1403,7 +1403,7 @@ private: env(pay(gw, bob, USD(50))); env.close(); - BEAST_EXPECT(expectLine(env, bob, USD(50))); + BEAST_EXPECT(expectHolding(env, bob, USD(50))); // Alice should not be able to create AMM without authorization. { @@ -1440,7 +1440,7 @@ private: BEAST_EXPECT( ammAlice.expectBalances(USD(1'050), XRP(1'000), ammAlice.tokens())); BEAST_EXPECT(expectOffers(env, bob, 0)); - BEAST_EXPECT(expectLine(env, bob, USD(0))); + BEAST_EXPECT(expectHolding(env, bob, USD(0))); } void @@ -1535,7 +1535,7 @@ private: // AMM offer is 51.282052XRP/11AUD, 11AUD/1.1 = 10AUD to bob BEAST_EXPECT( ammCarol.expectBalances(XRP(51), AUD(40), ammCarol.tokens())); - BEAST_EXPECT(expectLine(env, bob, AUD(10))); + BEAST_EXPECT(expectHolding(env, bob, AUD(10))); auto const result = find_paths(env, alice, bob, Account(bob)["USD"](25)); @@ -1950,10 +1950,10 @@ private: env(pay(alice, carol, USD(50)), path(~USD), sendmax(BTC(50))); - BEAST_EXPECT(expectLine(env, alice, BTC(50))); - BEAST_EXPECT(expectLine(env, bob, BTC(0))); - BEAST_EXPECT(expectLine(env, bob, USD(0))); - BEAST_EXPECT(expectLine(env, carol, USD(200))); + BEAST_EXPECT(expectHolding(env, alice, BTC(50))); + BEAST_EXPECT(expectHolding(env, bob, BTC(0))); + BEAST_EXPECT(expectHolding(env, bob, USD(0))); + BEAST_EXPECT(expectHolding(env, carol, USD(200))); BEAST_EXPECT( ammBob.expectBalances(BTC(150), USD(100), ammBob.tokens())); } @@ -1974,10 +1974,10 @@ private: env(pay(alice, carol, USD(50)), path(~XRP, ~USD), sendmax(BTC(50))); - BEAST_EXPECT(expectLine(env, alice, BTC(50))); - BEAST_EXPECT(expectLine(env, bob, BTC(0))); - BEAST_EXPECT(expectLine(env, bob, USD(0))); - BEAST_EXPECT(expectLine(env, carol, USD(200))); + BEAST_EXPECT(expectHolding(env, alice, BTC(50))); + BEAST_EXPECT(expectHolding(env, bob, BTC(0))); + BEAST_EXPECT(expectHolding(env, bob, USD(0))); + BEAST_EXPECT(expectHolding(env, carol, USD(200))); BEAST_EXPECT(ammBobBTC_XRP.expectBalances( BTC(150), XRP(100), ammBobBTC_XRP.tokens())); BEAST_EXPECT(ammBobXRP_USD.expectBalances( @@ -2003,8 +2003,8 @@ private: env, alice, xrpMinusFee(env, 10'000 - 50))); BEAST_EXPECT(expectLedgerEntryRoot( env, bob, XRP(10'000) - XRP(100) - ammCrtFee(env))); - BEAST_EXPECT(expectLine(env, bob, USD(0))); - BEAST_EXPECT(expectLine(env, carol, USD(200))); + BEAST_EXPECT(expectHolding(env, bob, USD(0))); + BEAST_EXPECT(expectHolding(env, carol, USD(200))); BEAST_EXPECT( ammBob.expectBalances(XRP(150), USD(100), ammBob.tokens())); } @@ -2024,10 +2024,10 @@ private: env(pay(alice, carol, XRP(50)), path(~XRP), sendmax(USD(50))); - BEAST_EXPECT(expectLine(env, alice, USD(50))); + BEAST_EXPECT(expectHolding(env, alice, USD(50))); BEAST_EXPECT(expectLedgerEntryRoot( env, bob, XRP(10'000) - XRP(150) - ammCrtFee(env))); - BEAST_EXPECT(expectLine(env, bob, USD(0))); + BEAST_EXPECT(expectHolding(env, bob, USD(0))); BEAST_EXPECT(expectLedgerEntryRoot(env, carol, XRP(10'000 + 50))); BEAST_EXPECT( ammBob.expectBalances(USD(150), XRP(100), ammBob.tokens())); @@ -2209,7 +2209,7 @@ private: sendmax(USD(0.4)), txflags(tfNoRippleDirect | tfPartialPayment)); - BEAST_EXPECT(expectLine(env, carol, EUR(1))); + BEAST_EXPECT(expectHolding(env, carol, EUR(1))); BEAST_EXPECT(ammBob.expectBalances( USD(8.4), XRPAmount{20}, ammBob.tokens())); } @@ -2244,7 +2244,7 @@ private: // alice buys 107.1428USD with 120GBP and pays 25% tr fee on 120GBP // 1,000 - 120*1.25 = 850GBP - BEAST_EXPECT(expectLine(env, alice, GBP(850))); + BEAST_EXPECT(expectHolding(env, alice, GBP(850))); if (!features[fixAMMv1_1]) { // 120GBP is swapped in for 107.1428USD @@ -2262,7 +2262,7 @@ private: } // 25% of 85.7142USD is paid in tr fee // 85.7142*1.25 = 107.1428USD - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount(USD, UINT64_C(1'085'714285714286), -12))); } @@ -2294,10 +2294,10 @@ private: // alice buys 120EUR with 120GBP via the offer // and pays 25% tr fee on 120GBP // 1,000 - 120*1.25 = 850GBP - BEAST_EXPECT(expectLine(env, alice, GBP(850))); + BEAST_EXPECT(expectHolding(env, alice, GBP(850))); // consumed offer is 120GBP/120EUR // ed doesn't pay tr fee - BEAST_EXPECT(expectLine(env, ed, EUR(880), GBP(1'120))); + BEAST_EXPECT(expectHolding(env, ed, EUR(880), GBP(1'120))); BEAST_EXPECT( expectOffers(env, ed, 1, {Amounts{GBP(880), EUR(880)}})); // 25% on 96EUR is paid in tr fee 96*1.25 = 120EUR @@ -2307,7 +2307,7 @@ private: STAmount{USD, UINT64_C(912'4087591240876), -13}, amm.tokens())); // 25% on 70.0729USD is paid in tr fee 70.0729*1.25 = 87.5912USD - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount(USD, UINT64_C(1'070'07299270073), -11))); } { @@ -2333,7 +2333,7 @@ private: txflags(tfNoRippleDirect | tfPartialPayment)); env.close(); - BEAST_EXPECT(expectLine(env, alice, GBP(850))); + BEAST_EXPECT(expectHolding(env, alice, GBP(850))); if (!features[fixAMMv1_1]) { // alice buys 107.1428EUR with 120GBP and pays 25% tr fee on @@ -2367,7 +2367,7 @@ private: amm2.tokens())); } // 25% on 63.1578USD is paid in tr fee 63.1578*1.25 = 78.9473USD - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount(USD, UINT64_C(1'063'157894736842), -12))); } { @@ -2386,7 +2386,7 @@ private: BEAST_EXPECT( amm.expectBalances(USD(1'100), EUR(1'000), amm.tokens())); // alice pays 25% tr fee on 100USD 1100-100*1.25 = 975USD - BEAST_EXPECT(expectLine(env, alice, USD(975), EUR(1'200))); + BEAST_EXPECT(expectHolding(env, alice, USD(975), EUR(1'200))); BEAST_EXPECT(expectOffers(env, alice, 0)); } @@ -2416,7 +2416,7 @@ private: // alice buys 125USD with 142.8571GBP and pays 25% tr fee // on 142.8571GBP // 1,000 - 142.8571*1.25 = 821.4285GBP - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, alice, STAmount(GBP, UINT64_C(821'4285714285712), -13))); // 142.8571GBP is swapped in for 125USD BEAST_EXPECT(amm.expectBalances( @@ -2425,7 +2425,7 @@ private: amm.tokens())); // 25% on 100USD is paid in tr fee // 100*1.25 = 125USD - BEAST_EXPECT(expectLine(env, carol, USD(1'100))); + BEAST_EXPECT(expectHolding(env, carol, USD(1'100))); } { // Payment via AMM with limit quality, deliver less @@ -2456,7 +2456,7 @@ private: // alice buys 28.125USD with 24GBP and pays 25% tr fee // on 24GBP // 1,200 - 24*1.25 = 1,170GBP - BEAST_EXPECT(expectLine(env, alice, GBP(1'170))); + BEAST_EXPECT(expectHolding(env, alice, GBP(1'170))); // 24GBP is swapped in for 28.125USD BEAST_EXPECT(amm.expectBalances( GBP(1'024), USD(1'171.875), amm.tokens())); @@ -2466,7 +2466,7 @@ private: // alice buys 28.125USD with 24GBP and pays 25% tr fee // on 24GBP // 1,200 - 24*1.25 =~ 1,170GBP - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, alice, STAmount{GBP, UINT64_C(1'169'999999999999), -12})); @@ -2478,7 +2478,7 @@ private: } // 25% on 22.5USD is paid in tr fee // 22.5*1.25 = 28.125USD - BEAST_EXPECT(expectLine(env, carol, USD(1'222.5))); + BEAST_EXPECT(expectHolding(env, carol, USD(1'222.5))); } { // Payment via offer and AMM with limit quality, deliver less @@ -2513,13 +2513,13 @@ private: // alice buys 70.4210EUR with 70.4210GBP via the offer // and pays 25% tr fee on 70.4210GBP // 1,400 - 70.4210*1.25 = 1400 - 88.0262 = 1311.9736GBP - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, alice, STAmount{GBP, UINT64_C(1'311'973684210527), -12})); // ed doesn't pay tr fee, the balances reflect consumed offer // 70.4210GBP/70.4210EUR - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, ed, STAmount{EUR, UINT64_C(1'329'578947368421), -12}, @@ -2543,13 +2543,13 @@ private: // alice buys 70.4210EUR with 70.4210GBP via the offer // and pays 25% tr fee on 70.4210GBP // 1,400 - 70.4210*1.25 = 1400 - 88.0262 = 1311.9736GBP - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, alice, STAmount{GBP, UINT64_C(1'311'973684210525), -12})); // ed doesn't pay tr fee, the balances reflect consumed offer // 70.4210GBP/70.4210EUR - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, ed, STAmount{EUR, UINT64_C(1'329'57894736842), -11}, @@ -2569,7 +2569,7 @@ private: amm.tokens())); } // 25% on 59.7321USD is paid in tr fee 59.7321*1.25 = 74.6651USD - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount(USD, UINT64_C(1'459'732142857143), -12))); } { @@ -2605,7 +2605,7 @@ private: // alice buys 53.3322EUR with 56.3368GBP via the amm // and pays 25% tr fee on 56.3368GBP // 1,400 - 56.3368*1.25 = 1400 - 70.4210 = 1329.5789GBP - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, alice, STAmount{GBP, UINT64_C(1'329'578947368421), -12})); @@ -2622,7 +2622,7 @@ private: // alice buys 53.3322EUR with 56.3368GBP via the amm // and pays 25% tr fee on 56.3368GBP // 1,400 - 56.3368*1.25 = 1400 - 70.4210 = 1329.5789GBP - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, alice, STAmount{GBP, UINT64_C(1'329'57894736842), -11})); @@ -2636,7 +2636,7 @@ private: } // 25% on 42.6658EUR is paid in tr fee 42.6658*1.25 = 53.3322EUR // 42.6658EUR/59.7321USD - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, ed, STAmount{USD, UINT64_C(1'340'267857142857), -12}, @@ -2649,7 +2649,7 @@ private: STAmount{EUR, UINT64_C(957'3341836734693), -13}, STAmount{USD, UINT64_C(1'340'267857142857), -12}}})); // 25% on 47.7857USD is paid in tr fee 47.7857*1.25 = 59.7321USD - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount(USD, UINT64_C(1'447'785714285714), -12))); } { @@ -2683,7 +2683,7 @@ private: // alice buys 53.3322EUR with 107.5308GBP // 25% on 86.0246GBP is paid in tr fee // 1,400 - 86.0246*1.25 = 1400 - 107.5308 = 1229.4691GBP - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, alice, STAmount{GBP, UINT64_C(1'292'469135802469), -12})); @@ -2704,7 +2704,7 @@ private: // alice buys 53.3322EUR with 107.5308GBP // 25% on 86.0246GBP is paid in tr fee // 1,400 - 86.0246*1.25 = 1400 - 107.5308 = 1229.4691GBP - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, alice, STAmount{GBP, UINT64_C(1'292'469135802466), -12})); @@ -2721,7 +2721,7 @@ private: amm2.tokens())); } // 25% on 66.7432USD is paid in tr fee 66.7432*1.25 = 83.4291USD - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount(USD, UINT64_C(1'466'743295019157), -12))); } { @@ -2778,7 +2778,7 @@ private: amm2.tokens())); } // 25% on 81.1111USD is paid in tr fee 81.1111*1.25 = 101.3888USD - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount{USD, UINT64_C(1'481'111111111111), -12})); } } @@ -2808,7 +2808,7 @@ private: BEAST_EXPECT( ammBob.expectBalances(XRP(1'050), USD(1'000), ammBob.tokens())); - BEAST_EXPECT(expectLine(env, carol, USD(2'050))); + BEAST_EXPECT(expectHolding(env, carol, USD(2'050))); BEAST_EXPECT(expectOffers(env, bob, 1, {{{XRP(100), USD(50)}}})); } } diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index c89aebf813..1fe37bb7c1 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -97,8 +97,8 @@ private: AMM ammAlice(env, alice, USD(20'000), BTC(0.5)); BEAST_EXPECT(ammAlice.expectBalances( USD(20'000), BTC(0.5), IOUAmount{100, 0})); - BEAST_EXPECT(expectLine(env, alice, USD(0))); - BEAST_EXPECT(expectLine(env, alice, BTC(0))); + BEAST_EXPECT(expectHolding(env, alice, USD(0))); + BEAST_EXPECT(expectHolding(env, alice, BTC(0))); } // Require authorization is set, account is authorized @@ -1394,7 +1394,7 @@ private: BEAST_EXPECT(ammAlice.expectBalances( XRP(11'000), USD(11'000), IOUAmount{11'000'000, 0})); // 30,000 less deposited 1,000 - BEAST_EXPECT(expectLine(env, carol, USD(29'000))); + BEAST_EXPECT(expectHolding(env, carol, USD(29'000))); // 30,000 less deposited 1,000 and 10 drops tx fee BEAST_EXPECT(expectLedgerEntryRoot( env, carol, XRPAmount{29'000'000'000 - baseFee})); @@ -1449,7 +1449,8 @@ private: IOUAmount{1, 7} + newLPTokens)); // 30,000 less deposited depositUSD - BEAST_EXPECT(expectLine(env, carol, USD(30'000) - depositUSD)); + BEAST_EXPECT( + expectHolding(env, carol, USD(30'000) - depositUSD)); // 30,000 less deposited depositXRP and 10 drops tx fee BEAST_EXPECT(expectLedgerEntryRoot( env, carol, XRP(30'000) - depositXRP - txfee(env, 1))); @@ -1553,15 +1554,15 @@ private: AMM ammAlice(env, alice, USD(20'000), BTC(0.5)); BEAST_EXPECT(ammAlice.expectBalances( USD(20'000), BTC(0.5), IOUAmount{100, 0})); - BEAST_EXPECT(expectLine(env, alice, USD(0))); - BEAST_EXPECT(expectLine(env, alice, BTC(0))); + BEAST_EXPECT(expectHolding(env, alice, USD(0))); + BEAST_EXPECT(expectHolding(env, alice, BTC(0))); fund(env, gw, {carol}, {USD(2'000), BTC(0.05)}, Fund::Acct); // no transfer fee on deposit ammAlice.deposit(carol, 10); BEAST_EXPECT(ammAlice.expectBalances( USD(22'000), BTC(0.55), IOUAmount{110, 0})); - BEAST_EXPECT(expectLine(env, carol, USD(0))); - BEAST_EXPECT(expectLine(env, carol, BTC(0))); + BEAST_EXPECT(expectHolding(env, carol, USD(0))); + BEAST_EXPECT(expectHolding(env, carol, BTC(0))); } // Tiny deposits @@ -2281,7 +2282,7 @@ private: BEAST_EXPECT( ammAlice.expectLPTokens(carol, IOUAmount{1'000'000, 0})); // 30,000 less deposited 1,000 - BEAST_EXPECT(expectLine(env, carol, USD(29'000))); + BEAST_EXPECT(expectHolding(env, carol, USD(29'000))); // 30,000 less deposited 1,000 and 10 drops tx fee BEAST_EXPECT(expectLedgerEntryRoot( env, carol, XRPAmount{29'000'000'000 - baseFee})); @@ -2290,7 +2291,7 @@ private: ammAlice.withdraw(carol, 1'000'000); BEAST_EXPECT( ammAlice.expectLPTokens(carol, IOUAmount(beast::Zero()))); - BEAST_EXPECT(expectLine(env, carol, USD(30'000))); + BEAST_EXPECT(expectHolding(env, carol, USD(30'000))); BEAST_EXPECT(expectLedgerEntryRoot( env, carol, XRPAmount{30'000'000'000 - 2 * baseFee})); }); @@ -2525,22 +2526,22 @@ private: AMM ammAlice(env, alice, USD(20'000), BTC(0.5)); BEAST_EXPECT(ammAlice.expectBalances( USD(20'000), BTC(0.5), IOUAmount{100, 0})); - BEAST_EXPECT(expectLine(env, alice, USD(0))); - BEAST_EXPECT(expectLine(env, alice, BTC(0))); + BEAST_EXPECT(expectHolding(env, alice, USD(0))); + BEAST_EXPECT(expectHolding(env, alice, BTC(0))); fund(env, gw, {carol}, {USD(2'000), BTC(0.05)}, Fund::Acct); // no transfer fee on deposit ammAlice.deposit(carol, 10); BEAST_EXPECT(ammAlice.expectBalances( USD(22'000), BTC(0.55), IOUAmount{110, 0})); - BEAST_EXPECT(expectLine(env, carol, USD(0))); - BEAST_EXPECT(expectLine(env, carol, BTC(0))); + BEAST_EXPECT(expectHolding(env, carol, USD(0))); + BEAST_EXPECT(expectHolding(env, carol, BTC(0))); // no transfer fee on withdraw ammAlice.withdraw(carol, 10); BEAST_EXPECT(ammAlice.expectBalances( USD(20'000), BTC(0.5), IOUAmount{100, 0})); BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0, 0})); - BEAST_EXPECT(expectLine(env, carol, USD(2'000))); - BEAST_EXPECT(expectLine(env, carol, BTC(0.05))); + BEAST_EXPECT(expectHolding(env, carol, USD(2'000))); + BEAST_EXPECT(expectHolding(env, carol, BTC(0.05))); } // Tiny withdraw @@ -3527,7 +3528,7 @@ private: // Alice doesn't have anymore lp tokens env(amm.bid({.account = alice, .bidMin = 500})); BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{500})); - BEAST_EXPECT(expectLine(env, alice, STAmount{lpIssue, 0})); + BEAST_EXPECT(expectHolding(env, alice, STAmount{lpIssue, 0})); // But trades with the discounted fee since she still owns the slot. // Alice pays 10011 drops in fees env(pay(alice, bob, USD(10)), path(~USD), sendmax(XRP(11))); @@ -3571,7 +3572,7 @@ private: env.current()->rules(), tapNONE, env.journal); - auto pf = AMMBid::preflight(pfctx); + auto pf = Transactor::invokePreflight(pfctx); BEAST_EXPECT(pf == temDISABLED); env.app().config().features.insert(featureAMM); } @@ -3586,7 +3587,7 @@ private: env.current()->rules(), tapNONE, env.journal); - auto pf = AMMBid::preflight(pfctx); + auto pf = Transactor::invokePreflight(pfctx); BEAST_EXPECT(pf != tesSUCCESS); } @@ -3601,7 +3602,7 @@ private: env.current()->rules(), tapNONE, env.journal); - auto pf = AMMBid::preflight(pfctx); + auto pf = Transactor::invokePreflight(pfctx); BEAST_EXPECT(pf == temBAD_AMM_TOKENS); } } @@ -3790,7 +3791,7 @@ private: BEAST_EXPECT(ammAlice.expectBalances( XRP(10'100), USD(10'000), ammAlice.tokens())); // Initial balance 30,000 + 100 - BEAST_EXPECT(expectLine(env, carol, USD(30'100))); + BEAST_EXPECT(expectHolding(env, carol, USD(30'100))); // Initial balance 30,000 - 100(sendmax) - 10(tx fee) BEAST_EXPECT(expectLedgerEntryRoot( env, bob, XRP(30'000) - XRP(100) - txfee(env, 1))); @@ -3810,7 +3811,7 @@ private: BEAST_EXPECT(ammAlice.expectBalances( XRP(10'100), USD(10'000), ammAlice.tokens())); // Initial balance 30,000 + 100 - BEAST_EXPECT(expectLine(env, carol, USD(30'100))); + BEAST_EXPECT(expectHolding(env, carol, USD(30'100))); // Initial balance 30,000 - 100(sendmax) - 10(tx fee) BEAST_EXPECT(expectLedgerEntryRoot( env, bob, XRP(30'000) - XRP(100) - txfee(env, 1))); @@ -3831,7 +3832,7 @@ private: BEAST_EXPECT(ammAlice.expectBalances( XRP(10'100), USD(10'000), ammAlice.tokens())); // Initial balance 30,000 + 100 - BEAST_EXPECT(expectLine(env, carol, USD(30'100))); + BEAST_EXPECT(expectHolding(env, carol, USD(30'100))); // Initial balance 30,000 - 100(sendmax) - 10(tx fee) BEAST_EXPECT(expectLedgerEntryRoot( env, bob, XRP(30'000) - XRP(100) - txfee(env, 1))); @@ -3857,7 +3858,7 @@ private: BEAST_EXPECT(ammAlice.expectBalances( XRP(10'010), USD(10'000), ammAlice.tokens())); // Initial balance 30,000 + 10(limited by limitQuality) - BEAST_EXPECT(expectLine(env, carol, USD(30'010))); + BEAST_EXPECT(expectHolding(env, carol, USD(30'010))); // Initial balance 30,000 - 10(limited by limitQuality) - 10(tx // fee) BEAST_EXPECT(expectLedgerEntryRoot( @@ -3897,7 +3898,7 @@ private: BEAST_EXPECT(ammAlice.expectBalances( XRP(10'010), USD(10'000), ammAlice.tokens())); // 10USD - 10% transfer fee - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount{USD, UINT64_C(30'009'09090909091), -11})); @@ -3984,7 +3985,7 @@ private: BEAST_EXPECT(expectOffers(env, alice, 1, {{expectedAmounts}})); } // Initial 30,000 + 100 - BEAST_EXPECT(expectLine(env, carol, STAmount{USD, 30'100})); + BEAST_EXPECT(expectHolding(env, carol, STAmount{USD, 30'100})); // Initial 1,000 - 30082730(AMM pool) - 70798251(offer) - 10(tx fee) BEAST_EXPECT(expectLedgerEntryRoot( env, @@ -4027,7 +4028,7 @@ private: STAmount(EUR, UINT64_C(49'98750312422), -11), STAmount(USD, UINT64_C(49'98750312422), -11)}}})); // Initial 30,000 + 99.99999999999 - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount{USD, UINT64_C(30'099'99999999999), -11})); @@ -4061,7 +4062,7 @@ private: BEAST_EXPECT(ammAlice.expectBalances( XRP(10'100), USD(10'000), ammAlice.tokens())); // Initial 30,000 + 200 - BEAST_EXPECT(expectLine(env, carol, USD(30'200))); + BEAST_EXPECT(expectHolding(env, carol, USD(30'200))); } else { @@ -4069,7 +4070,7 @@ private: XRP(10'100), STAmount(USD, UINT64_C(10'000'00000000001), -11), ammAlice.tokens())); - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount(USD, UINT64_C(30'199'99999999999), -11))); @@ -4104,7 +4105,7 @@ private: env.close(); BEAST_EXPECT(ammAlice.expectBalances( XRP(1'050), USD(1'000), ammAlice.tokens())); - BEAST_EXPECT(expectLine(env, carol, USD(2'200))); + BEAST_EXPECT(expectHolding(env, carol, USD(2'200))); BEAST_EXPECT(expectOffers(env, bob, 0)); } @@ -4118,7 +4119,7 @@ private: BEAST_EXPECT(ammAlice.expectBalances( XRP(10'100), USD(10'000), ammAlice.tokens())); // Initial 1,000 + 100 - BEAST_EXPECT(expectLine(env, bob, USD(1'100))); + BEAST_EXPECT(expectHolding(env, bob, USD(1'100))); // Initial 30,000 - 100(offer) - 10(tx fee) BEAST_EXPECT(expectLedgerEntryRoot( env, bob, XRP(30'000) - XRP(100) - txfee(env, 1))); @@ -4145,9 +4146,9 @@ private: BEAST_EXPECT(ammAlice.expectBalances( GBP(1'100), EUR(1'000), ammAlice.tokens())); // Initial 30,000 - 100(offer) - 25% transfer fee - BEAST_EXPECT(expectLine(env, carol, GBP(29'875))); + BEAST_EXPECT(expectHolding(env, carol, GBP(29'875))); // Initial 30,000 + 100(offer) - BEAST_EXPECT(expectLine(env, carol, EUR(30'100))); + BEAST_EXPECT(expectHolding(env, carol, EUR(30'100))); BEAST_EXPECT(expectOffers(env, bob, 0)); }, {{GBP(1'000), EUR(1'100)}}, @@ -4285,12 +4286,12 @@ private: // = 58.825 = ~29941.17 // carol bought ~72.93EUR at the cost of ~70.68GBP // the offer is partially consumed - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount{GBP, UINT64_C(29'941'16770347333), -11})); // Initial 30,000 + ~49.3(offers = 39.3(AMM) + 10(LOB)) - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount{EUR, UINT64_C(30'049'31517120716), -11})); @@ -4324,20 +4325,20 @@ private: // = 88.35 = ~29911.64 // carol bought ~72.93EUR at the cost of ~70.68GBP // the offer is partially consumed - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount{GBP, UINT64_C(29'911'64396400896), -11})); // Initial 30,000 + ~72.93(offers = 62.93(AMM) + 10(LOB)) - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount{EUR, UINT64_C(30'072'93416277865), -11})); } // Initial 2000 + 10 = 2010 - BEAST_EXPECT(expectLine(env, bob, GBP(2'010))); + BEAST_EXPECT(expectHolding(env, bob, GBP(2'010))); // Initial 2000 - 10 * 1.25 = 1987.5 - BEAST_EXPECT(expectLine(env, ed, EUR(1'987.5))); + BEAST_EXPECT(expectHolding(env, ed, EUR(1'987.5))); }, {{GBP(1'000), EUR(1'100)}}, 0, @@ -4363,8 +4364,8 @@ private: env.close(); BEAST_EXPECT(ammAlice.expectBalances( GBP(1'100), EUR(1'000), ammAlice.tokens())); - BEAST_EXPECT(expectLine(env, bob, GBP(75))); - BEAST_EXPECT(expectLine(env, carol, EUR(30'080))); + BEAST_EXPECT(expectHolding(env, bob, GBP(75))); + BEAST_EXPECT(expectHolding(env, carol, EUR(30'080))); }, {{GBP(1'000), EUR(1'100)}}, 0, @@ -4401,12 +4402,12 @@ private: sendmax(CAN(195.3125)), txflags(tfPartialPayment)); env.close(); - BEAST_EXPECT(expectLine(env, bob, CAN(0))); - BEAST_EXPECT(expectLine(env, dan, CAN(356.25), GBP(43.75))); + BEAST_EXPECT(expectHolding(env, bob, CAN(0))); + BEAST_EXPECT(expectHolding(env, dan, CAN(356.25), GBP(43.75))); BEAST_EXPECT(ammAlice.expectBalances( GBP(10'125), EUR(10'000), ammAlice.tokens())); - BEAST_EXPECT(expectLine(env, ed, EUR(300), USD(100))); - BEAST_EXPECT(expectLine(env, carol, USD(80))); + BEAST_EXPECT(expectHolding(env, ed, EUR(300), USD(100))); + BEAST_EXPECT(expectHolding(env, carol, USD(80))); }, {{GBP(10'000), EUR(10'125)}}, 0, @@ -4523,7 +4524,7 @@ private: BEAST_EXPECT(btc_usd.expectBalances( BTC(10'100), USD(10'000), btc_usd.tokens())); - BEAST_EXPECT(expectLine(env, carol, USD(300))); + BEAST_EXPECT(expectHolding(env, carol, USD(300))); } // Dependent AMM @@ -4594,7 +4595,7 @@ private: STAmount{EUR, UINT64_C(10'917'2945958102), -10}, eth_eur.tokens())); } - BEAST_EXPECT(expectLine(env, carol, USD(300))); + BEAST_EXPECT(expectHolding(env, carol, USD(300))); } // AMM offers limit @@ -4620,7 +4621,7 @@ private: XRP(10'030), STAmount{USD, UINT64_C(9'970'089730807577), -12}, ammAlice.tokens())); - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount{USD, UINT64_C(30'029'91026919241), -11})); @@ -4631,7 +4632,7 @@ private: XRP(10'030), STAmount{USD, UINT64_C(9'970'089730807827), -12}, ammAlice.tokens())); - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount{USD, UINT64_C(30'029'91026919217), -11})); @@ -4663,14 +4664,14 @@ private: if (!features[fixAMMv1_1]) { // Carol gets ~100USD - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount{USD, UINT64_C(30'099'99999999999), -11})); } else { - BEAST_EXPECT(expectLine(env, carol, USD(30'100))); + BEAST_EXPECT(expectHolding(env, carol, USD(30'100))); } BEAST_EXPECT(expectOffers( env, @@ -4717,7 +4718,7 @@ private: 1, {{{XRPAmount{50'074'628}, STAmount{USD, UINT64_C(50'07512950697), -11}}}})); - BEAST_EXPECT(expectLine(env, carol, USD(30'100))); + BEAST_EXPECT(expectHolding(env, carol, USD(30'100))); } } @@ -4809,11 +4810,11 @@ private: env(offer(carol, STAmount{token2, 100}, STAmount{token1, 100})); env.close(); BEAST_EXPECT( - expectLine(env, alice, STAmount{token1, 10'000'100}) && - expectLine(env, alice, STAmount{token2, 9'999'900})); + expectHolding(env, alice, STAmount{token1, 10'000'100}) && + expectHolding(env, alice, STAmount{token2, 9'999'900})); BEAST_EXPECT( - expectLine(env, carol, STAmount{token2, 1'000'100}) && - expectLine(env, carol, STAmount{token1, 999'900})); + expectHolding(env, carol, STAmount{token2, 1'000'100}) && + expectHolding(env, carol, STAmount{token1, 999'900})); BEAST_EXPECT( expectOffers(env, alice, 0) && expectOffers(env, carol, 0)); }); @@ -5034,7 +5035,7 @@ private: BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{1'000})); ammAlice.withdrawAll(carol, USD(3'000)); BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0})); - BEAST_EXPECT(expectLine(env, carol, USD(30'000))); + BEAST_EXPECT(expectHolding(env, carol, USD(30'000))); // Set fee to 1% ammAlice.vote(alice, 1'000); BEAST_EXPECT(ammAlice.expectTradingFee(1'000)); @@ -5043,12 +5044,12 @@ private: ammAlice.deposit(carol, USD(3'000)); BEAST_EXPECT(ammAlice.expectLPTokens( carol, IOUAmount{994'981155689671, -12})); - BEAST_EXPECT(expectLine(env, carol, USD(27'000))); + BEAST_EXPECT(expectHolding(env, carol, USD(27'000))); // Set fee to 0 ammAlice.vote(alice, 0); ammAlice.withdrawAll(carol, USD(0)); // Carol gets back less than the original deposit - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount{USD, UINT64_C(29'994'96220068281), -11})); @@ -5109,13 +5110,13 @@ private: ammAlice.deposit(carol, USD(3'000)); BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{1'000})); - BEAST_EXPECT(expectLine(env, carol, USD(27'000))); + BEAST_EXPECT(expectHolding(env, carol, USD(27'000))); // Set fee to 1% ammAlice.vote(alice, 1'000); BEAST_EXPECT(ammAlice.expectTradingFee(1'000)); // Single withdrawal. Carol gets ~5USD less than deposited. ammAlice.withdrawAll(carol, USD(0)); - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount{USD, UINT64_C(29'994'97487437186), -11})); @@ -5189,9 +5190,9 @@ private: {USD(1'000), EUR(1'000)}, Fund::Acct); // Alice contributed 1010EUR and 1000USD to the pool - BEAST_EXPECT(expectLine(env, alice, EUR(28'990))); - BEAST_EXPECT(expectLine(env, alice, USD(29'000))); - BEAST_EXPECT(expectLine(env, carol, USD(30'000))); + BEAST_EXPECT(expectHolding(env, alice, EUR(28'990))); + BEAST_EXPECT(expectHolding(env, alice, USD(29'000))); + BEAST_EXPECT(expectHolding(env, carol, USD(30'000))); // Carol pays to Alice with no fee env(pay(carol, alice, EUR(10)), path(~EUR), @@ -5199,9 +5200,9 @@ private: txflags(tfNoRippleDirect)); env.close(); // Alice has 10EUR more and Carol has 10USD less - BEAST_EXPECT(expectLine(env, alice, EUR(29'000))); - BEAST_EXPECT(expectLine(env, alice, USD(29'000))); - BEAST_EXPECT(expectLine(env, carol, USD(29'990))); + BEAST_EXPECT(expectHolding(env, alice, EUR(29'000))); + BEAST_EXPECT(expectHolding(env, alice, USD(29'000))); + BEAST_EXPECT(expectHolding(env, carol, USD(29'990))); // Set fee to 1% ammAlice.vote(alice, 1'000); @@ -5213,10 +5214,10 @@ private: txflags(tfNoRippleDirect)); env.close(); // Bob sends 10.1~EUR to pay 10USD - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, bob, STAmount{EUR, UINT64_C(989'8989898989899), -13})); // Carol got 10USD - BEAST_EXPECT(expectLine(env, carol, USD(30'000))); + BEAST_EXPECT(expectHolding(env, carol, USD(30'000))); BEAST_EXPECT(ammAlice.expectBalances( USD(1'000), STAmount{EUR, UINT64_C(1'010'10101010101), -11}, @@ -5233,8 +5234,8 @@ private: // No fee env(offer(carol, EUR(10), USD(10))); env.close(); - BEAST_EXPECT(expectLine(env, carol, USD(29'990))); - BEAST_EXPECT(expectLine(env, carol, EUR(30'010))); + BEAST_EXPECT(expectHolding(env, carol, USD(29'990))); + BEAST_EXPECT(expectHolding(env, carol, EUR(30'010))); // Change pool composition back env(offer(carol, USD(10), EUR(10))); env.close(); @@ -5245,11 +5246,11 @@ private: env.close(); // Alice gets fewer ~4.97EUR for ~5.02USD, the difference goes // to the pool - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount{USD, UINT64_C(29'995'02512562814), -11})); - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount{EUR, UINT64_C(30'004'97487437186), -11})); @@ -5299,16 +5300,16 @@ private: path(~USD), sendmax(EUR(15)), txflags(tfNoRippleDirect)); - BEAST_EXPECT(expectLine(env, ed, USD(2'010))); + BEAST_EXPECT(expectHolding(env, ed, USD(2'010))); if (!features[fixAMMv1_1]) { - BEAST_EXPECT(expectLine(env, bob, EUR(1'990))); + BEAST_EXPECT(expectHolding(env, bob, EUR(1'990))); BEAST_EXPECT(ammAlice.expectBalances( USD(1'000), EUR(1'005), ammAlice.tokens())); } else { - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, bob, STAmount(EUR, UINT64_C(1989'999999999999), -12))); BEAST_EXPECT(ammAlice.expectBalances( USD(1'000), @@ -5336,10 +5337,10 @@ private: path(~USD), sendmax(EUR(15)), txflags(tfNoRippleDirect)); - BEAST_EXPECT(expectLine(env, ed, USD(2'010))); + BEAST_EXPECT(expectHolding(env, ed, USD(2'010))); if (!features[fixAMMv1_1]) { - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, bob, STAmount{EUR, UINT64_C(1'989'987453007618), -12})); @@ -5350,7 +5351,7 @@ private: } else { - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, bob, STAmount{EUR, UINT64_C(1'989'987453007628), -12})); @@ -5381,8 +5382,8 @@ private: path(~USD), sendmax(EUR(15)), txflags(tfNoRippleDirect)); - BEAST_EXPECT(expectLine(env, ed, USD(2'010))); - BEAST_EXPECT(expectLine(env, bob, EUR(1'990))); + BEAST_EXPECT(expectHolding(env, ed, USD(2'010))); + BEAST_EXPECT(expectHolding(env, bob, EUR(1'990))); BEAST_EXPECT(ammAlice.expectBalances( USD(1'005), EUR(1'000), ammAlice.tokens())); BEAST_EXPECT(expectOffers(env, carol, 0)); @@ -5408,8 +5409,8 @@ private: path(~USD), sendmax(EUR(15)), txflags(tfNoRippleDirect)); - BEAST_EXPECT(expectLine(env, ed, USD(2'010))); - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding(env, ed, USD(2'010))); + BEAST_EXPECT(expectHolding( env, bob, STAmount{EUR, UINT64_C(1'989'993923296712), -12})); BEAST_EXPECT(ammAlice.expectBalances( USD(1'004), @@ -5480,47 +5481,47 @@ private: else BEAST_EXPECT(ammAlice.expectBalances( XRP(10'000), USD(10'000), IOUAmount{10'000'000})); - BEAST_EXPECT(expectLine(env, ben, USD(1'500'000))); - BEAST_EXPECT(expectLine(env, simon, USD(1'500'000))); - BEAST_EXPECT(expectLine(env, chris, USD(1'500'000))); - BEAST_EXPECT(expectLine(env, dan, USD(1'500'000))); + BEAST_EXPECT(expectHolding(env, ben, USD(1'500'000))); + BEAST_EXPECT(expectHolding(env, simon, USD(1'500'000))); + BEAST_EXPECT(expectHolding(env, chris, USD(1'500'000))); + BEAST_EXPECT(expectHolding(env, dan, USD(1'500'000))); if (!features[fixAMMv1_1] && !features[fixAMMv1_3]) - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount{USD, UINT64_C(30'000'00000000001), -11})); else if (features[fixAMMv1_1] && !features[fixAMMv1_3]) - BEAST_EXPECT(expectLine(env, carol, USD(30'000))); + BEAST_EXPECT(expectHolding(env, carol, USD(30'000))); else - BEAST_EXPECT(expectLine(env, carol, USD(30'000))); - BEAST_EXPECT(expectLine(env, ed, USD(1'500'000))); - BEAST_EXPECT(expectLine(env, paul, USD(1'500'000))); + BEAST_EXPECT(expectHolding(env, carol, USD(30'000))); + BEAST_EXPECT(expectHolding(env, ed, USD(1'500'000))); + BEAST_EXPECT(expectHolding(env, paul, USD(1'500'000))); if (!features[fixAMMv1_1] && !features[fixAMMv1_3]) - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, nataly, STAmount{USD, UINT64_C(1'500'000'000000002), -9})); else if (features[fixAMMv1_1] && !features[fixAMMv1_3]) - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, nataly, STAmount{USD, UINT64_C(1'500'000'000000005), -9})); else - BEAST_EXPECT(expectLine(env, nataly, USD(1'500'000))); + BEAST_EXPECT(expectHolding(env, nataly, USD(1'500'000))); ammAlice.withdrawAll(alice); BEAST_EXPECT(!ammAlice.ammExists()); if (!features[fixAMMv1_1]) - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, alice, STAmount{USD, UINT64_C(30'000'0000000013), -10})); else if (features[fixAMMv1_3]) - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, alice, STAmount{USD, UINT64_C(30'000'0000000003), -10})); else - BEAST_EXPECT(expectLine(env, alice, USD(30'000))); + BEAST_EXPECT(expectHolding(env, alice, USD(30'000))); // alice XRP balance is 30,000initial - 50 ammcreate fee - // 10drops fee BEAST_EXPECT( @@ -5883,7 +5884,7 @@ private: BEAST_EXPECT(amm->expectBalances( USD(1'000), ETH(1'000), amm->tokens())); } - BEAST_EXPECT(expectLine(env, bob, USD(2'100))); + BEAST_EXPECT(expectHolding(env, bob, USD(2'100))); q[i] = Quality(Amounts{ ETH(2'000) - env.balance(carol, ETH), env.balance(bob, USD) - USD(2'000)}); @@ -6056,7 +6057,7 @@ private: -13}}}})); } } - BEAST_EXPECT(expectLine(env, bob, USD(2'100))); + BEAST_EXPECT(expectHolding(env, bob, USD(2'100))); q[i] = Quality(Amounts{ ETH(2'000) - env.balance(carol, ETH), env.balance(bob, USD) - USD(2'000)}); @@ -6203,7 +6204,7 @@ private: sendmax(ETH(600))); env.close(); - BEAST_EXPECT(expectLine(env, bob, USD(2'100))); + BEAST_EXPECT(expectHolding(env, bob, USD(2'100))); if (i == 2 && !features[fixAMMv1_1]) { @@ -7484,7 +7485,7 @@ private: using namespace test::jtx; auto const testCase = [&](std::string suffix, FeatureBitset features) { - testcase("Failed pseudo-account allocation " + suffix); + testcase("Fail pseudo-account allocation " + suffix); std::string logs; Env env{*this, features, std::make_unique(&logs)}; env.fund(XRP(30'000), gw, alice); diff --git a/src/test/app/Credentials_test.cpp b/src/test/app/Credentials_test.cpp index 54826cbb12..102d516b89 100644 --- a/src/test/app/Credentials_test.cpp +++ b/src/test/app/Credentials_test.cpp @@ -19,10 +19,9 @@ #include -#include -#include - #include +#include +#include #include #include #include @@ -34,15 +33,6 @@ namespace ripple { namespace test { -static inline bool -checkVL( - std::shared_ptr const& sle, - SField const& field, - std::string const& expected) -{ - return strHex(expected) == strHex(sle->getFieldVL(field)); -} - struct Credentials_test : public beast::unit_test::suite { void diff --git a/src/test/app/DID_test.cpp b/src/test/app/DID_test.cpp index 1f28af2d6a..21fb6b584e 100644 --- a/src/test/app/DID_test.cpp +++ b/src/test/app/DID_test.cpp @@ -27,14 +27,6 @@ namespace ripple { namespace test { -bool -checkVL(Slice const& result, std::string expected) -{ - Serializer s; - s.addRaw(result); - return s.getString() == expected; -} - struct DID_test : public beast::unit_test::suite { void diff --git a/src/test/app/EscrowToken_test.cpp b/src/test/app/EscrowToken_test.cpp index e81064c825..6caedb53f1 100644 --- a/src/test/app/EscrowToken_test.cpp +++ b/src/test/app/EscrowToken_test.cpp @@ -20,9 +20,9 @@ #include #include -#include -#include +#include +#include #include #include #include @@ -3426,7 +3426,7 @@ struct EscrowToken_test : public beast::unit_test::suite auto const preAliceMPT = env.balance(alice, MPT); auto const preOutstanding = env.balance(gw, MPT); auto const preEscrowed = issuerMPTEscrowed(env, MPT); - BEAST_EXPECT(preOutstanding == MPT(10'000)); + BEAST_EXPECT(preOutstanding == MPT(-10'000)); BEAST_EXPECT(preEscrowed == 0); env(escrow::create(alice, gw, MPT(1'000)), @@ -3449,7 +3449,7 @@ struct EscrowToken_test : public beast::unit_test::suite BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1'000)); BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); - BEAST_EXPECT(env.balance(gw, MPT) == preOutstanding - MPT(1'000)); + BEAST_EXPECT(env.balance(gw, MPT) == preOutstanding + MPT(1'000)); BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == preEscrowed); } } @@ -3501,6 +3501,10 @@ struct EscrowToken_test : public beast::unit_test::suite BEAST_EXPECT( transferRate.value == std::uint32_t(1'000'000'000 * 1.25)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 125); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 125); + BEAST_EXPECT(env.balance(gw, MPT) == MPT(-20'000)); + // bob can finish escrow env(escrow::finish(bob, alice, seq1), escrow::condition(escrow::cb1), @@ -3510,6 +3514,15 @@ struct EscrowToken_test : public beast::unit_test::suite BEAST_EXPECT(env.balance(alice, MPT) == preAlice - delta); BEAST_EXPECT(env.balance(bob, MPT) == MPT(10'100)); + + auto const escrowedWithFix = + env.current()->rules().enabled(fixTokenEscrowV1) ? 0 : 25; + auto const outstandingWithFix = + env.current()->rules().enabled(fixTokenEscrowV1) ? MPT(19'975) + : MPT(20'000); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == escrowedWithFix); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == escrowedWithFix); + BEAST_EXPECT(env.balance(gw, MPT) == -outstandingWithFix); } // test locked rate: cancel @@ -3554,6 +3567,60 @@ struct EscrowToken_test : public beast::unit_test::suite BEAST_EXPECT(env.balance(alice, MPT) == preAlice); BEAST_EXPECT(env.balance(bob, MPT) == preBob); + BEAST_EXPECT(env.balance(gw, MPT) == MPT(-20'000)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0); + } + + // test locked rate: issuer is destination + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.transferFee = 25000, + .ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + // alice can create escrow w/ xfer rate + auto const preAlice = env.balance(alice, MPT); + auto const seq1 = env.seq(alice); + auto const delta = MPT(125); + env(escrow::create(alice, gw, MPT(125)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + auto const transferRate = escrow::rate(env, alice, seq1); + BEAST_EXPECT( + transferRate.value == std::uint32_t(1'000'000'000 * 1.25)); + + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 125); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 125); + BEAST_EXPECT(env.balance(gw, MPT) == MPT(-20'000)); + + // bob can finish escrow + env(escrow::finish(gw, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAlice - delta); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == MPT(-19'875)); } } @@ -3759,7 +3826,7 @@ struct EscrowToken_test : public beast::unit_test::suite BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 10); BEAST_EXPECT(env.balance(bob, MPT) == MPT(0)); BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); - BEAST_EXPECT(env.balance(gw, MPT) == MPT(10)); + BEAST_EXPECT(env.balance(gw, MPT) == MPT(-10)); mptGw.authorize({.account = bob, .flags = tfMPTUnauthorize}); mptGw.destroy( {.id = mptGw.issuanceID(), @@ -3878,6 +3945,7 @@ public: FeatureBitset const all{testable_amendments()}; testIOUWithFeats(all); testMPTWithFeats(all); + testMPTWithFeats(all - fixTokenEscrowV1); } }; diff --git a/src/test/app/Escrow_test.cpp b/src/test/app/Escrow_test.cpp index 3eaf0f13ea..cea3a835a6 100644 --- a/src/test/app/Escrow_test.cpp +++ b/src/test/app/Escrow_test.cpp @@ -20,8 +20,8 @@ #include #include -#include +#include #include #include #include @@ -253,6 +253,14 @@ struct Escrow_test : public beast::unit_test::suite BEAST_EXPECT(sle); BEAST_EXPECT((*sle)[sfSourceTag] == 1); BEAST_EXPECT((*sle)[sfDestinationTag] == 2); + if (features[fixIncludeKeyletFields]) + { + BEAST_EXPECT((*sle)[sfSequence] == seq); + } + else + { + BEAST_EXPECT(!sle->isFieldPresent(sfSequence)); + } } void @@ -1718,6 +1726,7 @@ public: FeatureBitset const all{testable_amendments()}; testWithFeats(all); testWithFeats(all - featureTokenEscrow); + testTags(all - fixIncludeKeyletFields); } }; diff --git a/src/test/app/FeeVote_test.cpp b/src/test/app/FeeVote_test.cpp index ba3d379219..4fe0a62e3b 100644 --- a/src/test/app/FeeVote_test.cpp +++ b/src/test/app/FeeVote_test.cpp @@ -19,11 +19,203 @@ #include +#include +#include +#include + #include +#include +#include +#include +#include +#include +#include namespace ripple { namespace test { +struct FeeSettingsFields +{ + std::optional baseFee = std::nullopt; + std::optional reserveBase = std::nullopt; + std::optional reserveIncrement = std::nullopt; + std::optional referenceFeeUnits = std::nullopt; + std::optional baseFeeDrops = std::nullopt; + std::optional reserveBaseDrops = std::nullopt; + std::optional reserveIncrementDrops = std::nullopt; +}; + +STTx +createFeeTx( + Rules const& rules, + std::uint32_t seq, + FeeSettingsFields const& fields) +{ + auto fill = [&](auto& obj) { + obj.setAccountID(sfAccount, AccountID()); + obj.setFieldU32(sfLedgerSequence, seq); + + if (rules.enabled(featureXRPFees)) + { + // New XRPFees format - all three fields are REQUIRED + obj.setFieldAmount( + sfBaseFeeDrops, + fields.baseFeeDrops ? *fields.baseFeeDrops : XRPAmount{0}); + obj.setFieldAmount( + sfReserveBaseDrops, + fields.reserveBaseDrops ? *fields.reserveBaseDrops + : XRPAmount{0}); + obj.setFieldAmount( + sfReserveIncrementDrops, + fields.reserveIncrementDrops ? *fields.reserveIncrementDrops + : XRPAmount{0}); + } + else + { + // Legacy format - all four fields are REQUIRED + obj.setFieldU64(sfBaseFee, fields.baseFee ? *fields.baseFee : 0); + obj.setFieldU32( + sfReserveBase, fields.reserveBase ? *fields.reserveBase : 0); + obj.setFieldU32( + sfReserveIncrement, + fields.reserveIncrement ? *fields.reserveIncrement : 0); + obj.setFieldU32( + sfReferenceFeeUnits, + fields.referenceFeeUnits ? *fields.referenceFeeUnits : 0); + } + }; + return STTx(ttFEE, fill); +} + +STTx +createInvalidFeeTx( + Rules const& rules, + std::uint32_t seq, + bool missingRequiredFields = true, + bool wrongFeatureFields = false, + std::uint32_t uniqueValue = 42) +{ + auto fill = [&](auto& obj) { + obj.setAccountID(sfAccount, AccountID()); + obj.setFieldU32(sfLedgerSequence, seq); + + if (wrongFeatureFields) + { + if (rules.enabled(featureXRPFees)) + { + obj.setFieldU64(sfBaseFee, 10 + uniqueValue); + obj.setFieldU32(sfReserveBase, 200000); + obj.setFieldU32(sfReserveIncrement, 50000); + obj.setFieldU32(sfReferenceFeeUnits, 10); + } + else + { + obj.setFieldAmount(sfBaseFeeDrops, XRPAmount{10 + uniqueValue}); + obj.setFieldAmount(sfReserveBaseDrops, XRPAmount{200000}); + obj.setFieldAmount(sfReserveIncrementDrops, XRPAmount{50000}); + } + } + else if (!missingRequiredFields) + { + // Create valid transaction (all required fields present) + if (rules.enabled(featureXRPFees)) + { + obj.setFieldAmount(sfBaseFeeDrops, XRPAmount{10 + uniqueValue}); + obj.setFieldAmount(sfReserveBaseDrops, XRPAmount{200000}); + obj.setFieldAmount(sfReserveIncrementDrops, XRPAmount{50000}); + } + else + { + obj.setFieldU64(sfBaseFee, 10 + uniqueValue); + obj.setFieldU32(sfReserveBase, 200000); + obj.setFieldU32(sfReserveIncrement, 50000); + obj.setFieldU32(sfReferenceFeeUnits, 10); + } + } + // If missingRequiredFields is true, we don't add the required fields + // (default behavior) + }; + return STTx(ttFEE, fill); +} + +bool +applyFeeAndTestResult(jtx::Env& env, OpenView& view, STTx const& tx) +{ + auto const res = + apply(env.app(), view, tx, ApplyFlags::tapNONE, env.journal); + return res.ter == tesSUCCESS; +} + +bool +verifyFeeObject( + std::shared_ptr const& ledger, + Rules const& rules, + FeeSettingsFields const& expected) +{ + auto const feeObject = ledger->read(keylet::fees()); + if (!feeObject) + return false; + + auto checkEquality = [&](auto const& field, auto const& expected) { + if (!feeObject->isFieldPresent(field)) + return false; + return feeObject->at(field) == expected; + }; + + if (rules.enabled(featureXRPFees)) + { + if (feeObject->isFieldPresent(sfBaseFee) || + feeObject->isFieldPresent(sfReserveBase) || + feeObject->isFieldPresent(sfReserveIncrement) || + feeObject->isFieldPresent(sfReferenceFeeUnits)) + return false; + + if (!checkEquality( + sfBaseFeeDrops, expected.baseFeeDrops.value_or(XRPAmount{0}))) + return false; + if (!checkEquality( + sfReserveBaseDrops, + expected.reserveBaseDrops.value_or(XRPAmount{0}))) + return false; + if (!checkEquality( + sfReserveIncrementDrops, + expected.reserveIncrementDrops.value_or(XRPAmount{0}))) + return false; + } + else + { + if (feeObject->isFieldPresent(sfBaseFeeDrops) || + feeObject->isFieldPresent(sfReserveBaseDrops) || + feeObject->isFieldPresent(sfReserveIncrementDrops)) + return false; + + // Read sfBaseFee as a hex string and compare to expected.baseFee + if (!checkEquality(sfBaseFee, expected.baseFee)) + return false; + if (!checkEquality(sfReserveBase, expected.reserveBase)) + return false; + if (!checkEquality(sfReserveIncrement, expected.reserveIncrement)) + return false; + if (!checkEquality(sfReferenceFeeUnits, expected.referenceFeeUnits)) + return false; + } + + return true; +} + +std::vector +getTxs(std::shared_ptr const& txSet) +{ + std::vector txs; + for (auto i = txSet->begin(); i != txSet->end(); ++i) + { + auto const data = i->slice(); + auto serialIter = SerialIter(data); + txs.push_back(STTx(serialIter)); + } + return txs; +}; + class FeeVote_test : public beast::unit_test::suite { void @@ -93,10 +285,517 @@ class FeeVote_test : public beast::unit_test::suite } } + void + testBasic() + { + testcase("Basic SetFee transaction"); + + // Test with XRPFees disabled (legacy format) + { + jtx::Env env(*this, jtx::testable_amendments() - featureXRPFees); + auto ledger = std::make_shared( + create_genesis, + env.app().config(), + std::vector{}, + env.app().getNodeFamily()); + + // Create the next ledger to apply transaction to + ledger = std::make_shared( + *ledger, env.app().timeKeeper().closeTime()); + + // Test successful fee transaction with legacy fields + + FeeSettingsFields fields{ + .baseFee = 10, + .reserveBase = 200000, + .reserveIncrement = 50000, + .referenceFeeUnits = 10}; + auto feeTx = createFeeTx(ledger->rules(), ledger->seq(), fields); + + OpenView accum(ledger.get()); + BEAST_EXPECT(applyFeeAndTestResult(env, accum, feeTx)); + accum.apply(*ledger); + + // Verify fee object was created/updated correctly + BEAST_EXPECT(verifyFeeObject(ledger, ledger->rules(), fields)); + } + + // Test with XRPFees enabled (new format) + { + jtx::Env env(*this, jtx::testable_amendments() | featureXRPFees); + auto ledger = std::make_shared( + create_genesis, + env.app().config(), + std::vector{}, + env.app().getNodeFamily()); + + // Create the next ledger to apply transaction to + ledger = std::make_shared( + *ledger, env.app().timeKeeper().closeTime()); + + FeeSettingsFields fields{ + .baseFeeDrops = XRPAmount{10}, + .reserveBaseDrops = XRPAmount{200000}, + .reserveIncrementDrops = XRPAmount{50000}}; + // Test successful fee transaction with new fields + auto feeTx = createFeeTx(ledger->rules(), ledger->seq(), fields); + + OpenView accum(ledger.get()); + BEAST_EXPECT(applyFeeAndTestResult(env, accum, feeTx)); + accum.apply(*ledger); + + // Verify fee object was created/updated correctly + BEAST_EXPECT(verifyFeeObject(ledger, ledger->rules(), fields)); + } + } + + void + testTransactionValidation() + { + testcase("Fee Transaction Validation"); + + { + jtx::Env env(*this, jtx::testable_amendments() - featureXRPFees); + auto ledger = std::make_shared( + create_genesis, + env.app().config(), + std::vector{}, + env.app().getNodeFamily()); + + // Create the next ledger to apply transaction to + ledger = std::make_shared( + *ledger, env.app().timeKeeper().closeTime()); + + // Test transaction with missing required legacy fields + auto invalidTx = createInvalidFeeTx( + ledger->rules(), ledger->seq(), true, false, 1); + OpenView accum(ledger.get()); + BEAST_EXPECT(!applyFeeAndTestResult(env, accum, invalidTx)); + + // Test transaction with new format fields when XRPFees is disabled + auto disallowedTx = createInvalidFeeTx( + ledger->rules(), ledger->seq(), false, true, 2); + BEAST_EXPECT(!applyFeeAndTestResult(env, accum, disallowedTx)); + } + + { + jtx::Env env(*this, jtx::testable_amendments() | featureXRPFees); + auto ledger = std::make_shared( + create_genesis, + env.app().config(), + std::vector{}, + env.app().getNodeFamily()); + + // Create the next ledger to apply transaction to + ledger = std::make_shared( + *ledger, env.app().timeKeeper().closeTime()); + + // Test transaction with missing required new fields + auto invalidTx = createInvalidFeeTx( + ledger->rules(), ledger->seq(), true, false, 3); + OpenView accum(ledger.get()); + BEAST_EXPECT(!applyFeeAndTestResult(env, accum, invalidTx)); + + // Test transaction with legacy fields when XRPFees is enabled + auto disallowedTx = createInvalidFeeTx( + ledger->rules(), ledger->seq(), false, true, 4); + BEAST_EXPECT(!applyFeeAndTestResult(env, accum, disallowedTx)); + } + } + + void + testPseudoTransactionProperties() + { + testcase("Pseudo Transaction Properties"); + + jtx::Env env(*this, jtx::testable_amendments()); + auto ledger = std::make_shared( + create_genesis, + env.app().config(), + std::vector{}, + env.app().getNodeFamily()); + + // Create the next ledger to apply transaction to + ledger = std::make_shared( + *ledger, env.app().timeKeeper().closeTime()); + + auto feeTx = createFeeTx( + ledger->rules(), + ledger->seq(), + {.baseFeeDrops = XRPAmount{10}, + .reserveBaseDrops = XRPAmount{200000}, + .reserveIncrementDrops = XRPAmount{50000}}); + + // Verify pseudo-transaction properties + BEAST_EXPECT(feeTx.getAccountID(sfAccount) == AccountID()); + BEAST_EXPECT(feeTx.getFieldAmount(sfFee) == XRPAmount{0}); + BEAST_EXPECT(feeTx.getSigningPubKey().empty()); + BEAST_EXPECT(feeTx.getSignature().empty()); + BEAST_EXPECT(!feeTx.isFieldPresent(sfSigners)); + BEAST_EXPECT(feeTx.getFieldU32(sfSequence) == 0); + BEAST_EXPECT(!feeTx.isFieldPresent(sfPreviousTxnID)); + + // But can be applied to a closed ledger + { + OpenView closedAccum(ledger.get()); + BEAST_EXPECT(applyFeeAndTestResult(env, closedAccum, feeTx)); + } + } + + void + testMultipleFeeUpdates() + { + testcase("Multiple Fee Updates"); + + jtx::Env env(*this, jtx::testable_amendments() | featureXRPFees); + auto ledger = std::make_shared( + create_genesis, + env.app().config(), + std::vector{}, + env.app().getNodeFamily()); + + ledger = std::make_shared( + *ledger, env.app().timeKeeper().closeTime()); + + FeeSettingsFields fields1{ + .baseFeeDrops = XRPAmount{10}, + .reserveBaseDrops = XRPAmount{200000}, + .reserveIncrementDrops = XRPAmount{50000}}; + auto feeTx1 = createFeeTx(ledger->rules(), ledger->seq(), fields1); + + { + OpenView accum(ledger.get()); + BEAST_EXPECT(applyFeeAndTestResult(env, accum, feeTx1)); + accum.apply(*ledger); + } + + BEAST_EXPECT(verifyFeeObject(ledger, ledger->rules(), fields1)); + + // Apply second fee transaction with different values + ledger = std::make_shared( + *ledger, env.app().timeKeeper().closeTime()); + + FeeSettingsFields fields2{ + .baseFeeDrops = XRPAmount{20}, + .reserveBaseDrops = XRPAmount{300000}, + .reserveIncrementDrops = XRPAmount{75000}}; + auto feeTx2 = createFeeTx(ledger->rules(), ledger->seq(), fields2); + + { + OpenView accum(ledger.get()); + BEAST_EXPECT(applyFeeAndTestResult(env, accum, feeTx2)); + accum.apply(*ledger); + } + + // Verify second update overwrote the first + BEAST_EXPECT(verifyFeeObject(ledger, ledger->rules(), fields2)); + } + + void + testWrongLedgerSequence() + { + testcase("Wrong Ledger Sequence"); + + jtx::Env env(*this, jtx::testable_amendments() | featureXRPFees); + auto ledger = std::make_shared( + create_genesis, + env.app().config(), + std::vector{}, + env.app().getNodeFamily()); + + ledger = std::make_shared( + *ledger, env.app().timeKeeper().closeTime()); + + // Test transaction with wrong ledger sequence + auto feeTx = createFeeTx( + ledger->rules(), + ledger->seq() + 5, // Wrong sequence (should be ledger->seq()) + {.baseFeeDrops = XRPAmount{10}, + .reserveBaseDrops = XRPAmount{200000}, + .reserveIncrementDrops = XRPAmount{50000}}); + + OpenView accum(ledger.get()); + + // The transaction should still succeed as long as other fields are + // valid + // The ledger sequence field is only used for informational purposes + BEAST_EXPECT(applyFeeAndTestResult(env, accum, feeTx)); + } + + void + testPartialFieldUpdates() + { + testcase("Partial Field Updates"); + + jtx::Env env(*this, jtx::testable_amendments() | featureXRPFees); + auto ledger = std::make_shared( + create_genesis, + env.app().config(), + std::vector{}, + env.app().getNodeFamily()); + + ledger = std::make_shared( + *ledger, env.app().timeKeeper().closeTime()); + + FeeSettingsFields fields1{ + .baseFeeDrops = XRPAmount{10}, + .reserveBaseDrops = XRPAmount{200000}, + .reserveIncrementDrops = XRPAmount{50000}}; + auto feeTx1 = createFeeTx(ledger->rules(), ledger->seq(), fields1); + + { + OpenView accum(ledger.get()); + BEAST_EXPECT(applyFeeAndTestResult(env, accum, feeTx1)); + accum.apply(*ledger); + } + + BEAST_EXPECT(verifyFeeObject(ledger, ledger->rules(), fields1)); + + ledger = std::make_shared( + *ledger, env.app().timeKeeper().closeTime()); + + // Apply partial update (only some fields) + FeeSettingsFields fields2{ + .baseFeeDrops = XRPAmount{20}, + .reserveBaseDrops = XRPAmount{200000}}; + auto feeTx2 = createFeeTx(ledger->rules(), ledger->seq(), fields2); + + { + OpenView accum(ledger.get()); + BEAST_EXPECT(applyFeeAndTestResult(env, accum, feeTx2)); + accum.apply(*ledger); + } + + // Verify the partial update worked + BEAST_EXPECT(verifyFeeObject(ledger, ledger->rules(), fields2)); + } + + void + testSingleInvalidTransaction() + { + testcase("Single Invalid Transaction"); + + jtx::Env env(*this, jtx::testable_amendments() | featureXRPFees); + auto ledger = std::make_shared( + create_genesis, + env.app().config(), + std::vector{}, + env.app().getNodeFamily()); + + ledger = std::make_shared( + *ledger, env.app().timeKeeper().closeTime()); + + // Test invalid transaction with non-zero account - this should fail + // validation + auto invalidTx = STTx(ttFEE, [&](auto& obj) { + obj.setAccountID( + sfAccount, + AccountID(1)); // Should be zero (this makes it invalid) + obj.setFieldU32(sfLedgerSequence, ledger->seq()); + obj.setFieldAmount(sfBaseFeeDrops, XRPAmount{10}); + obj.setFieldAmount(sfReserveBaseDrops, XRPAmount{200000}); + obj.setFieldAmount(sfReserveIncrementDrops, XRPAmount{50000}); + }); + + OpenView accum(ledger.get()); + BEAST_EXPECT(!applyFeeAndTestResult(env, accum, invalidTx)); + } + + void + testDoValidation() + { + testcase("doValidation"); + + using namespace jtx; + + FeeSetup setup; + setup.reference_fee = 42; + setup.account_reserve = 1234567; + setup.owner_reserve = 7654321; + + // Test with XRPFees enabled + { + Env env(*this, testable_amendments() | featureXRPFees); + auto feeVote = make_FeeVote(setup, env.app().journal("FeeVote")); + + auto ledger = std::make_shared( + create_genesis, + env.app().config(), + std::vector{}, + env.app().getNodeFamily()); + + auto sec = randomSecretKey(); + auto pub = derivePublicKey(KeyType::secp256k1, sec); + + auto val = std::make_shared( + env.app().timeKeeper().now(), + pub, + sec, + calcNodeID(pub), + [](STValidation& v) { + v.setFieldU32(sfLedgerSequence, 12345); + }); + + // Use the current ledger's fees as the "current" fees for + // doValidation + auto const& currentFees = ledger->fees(); + + feeVote->doValidation(currentFees, ledger->rules(), *val); + + BEAST_EXPECT(val->isFieldPresent(sfBaseFeeDrops)); + BEAST_EXPECT( + val->getFieldAmount(sfBaseFeeDrops) == + XRPAmount(setup.reference_fee)); + } + + // Test with XRPFees disabled (legacy format) + { + Env env(*this, testable_amendments() - featureXRPFees); + auto feeVote = make_FeeVote(setup, env.app().journal("FeeVote")); + + auto ledger = std::make_shared( + create_genesis, + env.app().config(), + std::vector{}, + env.app().getNodeFamily()); + + auto sec = randomSecretKey(); + auto pub = derivePublicKey(KeyType::secp256k1, sec); + + auto val = std::make_shared( + env.app().timeKeeper().now(), + pub, + sec, + calcNodeID(pub), + [](STValidation& v) { + v.setFieldU32(sfLedgerSequence, 12345); + }); + + auto const& currentFees = ledger->fees(); + + feeVote->doValidation(currentFees, ledger->rules(), *val); + + // In legacy mode, should vote using legacy fields + BEAST_EXPECT(val->isFieldPresent(sfBaseFee)); + BEAST_EXPECT(val->getFieldU64(sfBaseFee) == setup.reference_fee); + } + } + + void + testDoVoting() + { + testcase("doVoting"); + + using namespace jtx; + + FeeSetup setup; + setup.reference_fee = 42; + setup.account_reserve = 1234567; + setup.owner_reserve = 7654321; + + Env env(*this, testable_amendments() | featureXRPFees); + + // establish what the current fees are + BEAST_EXPECT( + env.current()->fees().base == XRPAmount{UNIT_TEST_REFERENCE_FEE}); + BEAST_EXPECT(env.current()->fees().reserve == XRPAmount{200'000'000}); + BEAST_EXPECT(env.current()->fees().increment == XRPAmount{50'000'000}); + + auto feeVote = make_FeeVote(setup, env.app().journal("FeeVote")); + auto ledger = std::make_shared( + create_genesis, + env.app().config(), + std::vector{}, + env.app().getNodeFamily()); + + // doVoting requires a flag ledger (every 256th ledger) + // We need to create a ledger at sequence 256 to make it a flag ledger + for (int i = 0; i < 256 - 1; ++i) + { + ledger = std::make_shared( + *ledger, env.app().timeKeeper().closeTime()); + } + BEAST_EXPECT(ledger->isFlagLedger()); + + // Create some mock validations with fee votes + std::vector> validations; + + for (int i = 0; i < 5; i++) + { + auto sec = randomSecretKey(); + auto pub = derivePublicKey(KeyType::secp256k1, sec); + + auto val = std::make_shared( + env.app().timeKeeper().now(), + pub, + sec, + calcNodeID(pub), + [&](STValidation& v) { + v.setFieldU32(sfLedgerSequence, ledger->seq()); + // Vote for different fees than current + v.setFieldAmount( + sfBaseFeeDrops, XRPAmount{setup.reference_fee}); + v.setFieldAmount( + sfReserveBaseDrops, XRPAmount{setup.account_reserve}); + v.setFieldAmount( + sfReserveIncrementDrops, + XRPAmount{setup.owner_reserve}); + }); + if (i % 2) + val->setTrusted(); + validations.push_back(val); + } + + auto txSet = std::make_shared( + SHAMapType::TRANSACTION, env.app().getNodeFamily()); + + // This should not throw since we have a flag ledger + feeVote->doVoting(ledger, validations, txSet); + + auto const txs = getTxs(txSet); + BEAST_EXPECT(txs.size() == 1); + auto const& feeTx = txs[0]; + + BEAST_EXPECT(feeTx.getTxnType() == ttFEE); + + BEAST_EXPECT(feeTx.getAccountID(sfAccount) == AccountID()); + BEAST_EXPECT(feeTx.getFieldU32(sfLedgerSequence) == ledger->seq() + 1); + + BEAST_EXPECT(feeTx.isFieldPresent(sfBaseFeeDrops)); + BEAST_EXPECT(feeTx.isFieldPresent(sfReserveBaseDrops)); + BEAST_EXPECT(feeTx.isFieldPresent(sfReserveIncrementDrops)); + + // The legacy fields should NOT be present + BEAST_EXPECT(!feeTx.isFieldPresent(sfBaseFee)); + BEAST_EXPECT(!feeTx.isFieldPresent(sfReserveBase)); + BEAST_EXPECT(!feeTx.isFieldPresent(sfReserveIncrement)); + BEAST_EXPECT(!feeTx.isFieldPresent(sfReferenceFeeUnits)); + + // Check the values + BEAST_EXPECT( + feeTx.getFieldAmount(sfBaseFeeDrops) == + XRPAmount{setup.reference_fee}); + BEAST_EXPECT( + feeTx.getFieldAmount(sfReserveBaseDrops) == + XRPAmount{setup.account_reserve}); + BEAST_EXPECT( + feeTx.getFieldAmount(sfReserveIncrementDrops) == + XRPAmount{setup.owner_reserve}); + } + void run() override { testSetup(); + testBasic(); + testTransactionValidation(); + testPseudoTransactionProperties(); + testMultipleFeeUpdates(); + testWrongLedgerSequence(); + testPartialFieldUpdates(); + testSingleInvalidTransaction(); + testDoValidation(); + testDoVoting(); } }; diff --git a/src/test/app/Flow_test.cpp b/src/test/app/Flow_test.cpp index 0f40d70b57..23095b0145 100644 --- a/src/test/app/Flow_test.cpp +++ b/src/test/app/Flow_test.cpp @@ -23,10 +23,10 @@ #include #include #include -#include -#include #include +#include +#include #include namespace ripple { diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index ae2a1c45df..c91149b2f7 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -31,6 +31,7 @@ #include namespace ripple { +namespace test { class Invariants_test : public beast::unit_test::suite { @@ -112,13 +113,13 @@ class Invariants_test : public beast::unit_test::suite { terActual = ac.checkInvariants(terActual, fee); BEAST_EXPECT(terExpect == terActual); + auto const messages = sink.messages().str(); BEAST_EXPECT( - sink.messages().str().starts_with("Invariant failed:") || - sink.messages().str().starts_with( - "Transaction caused an exception")); + messages.starts_with("Invariant failed:") || + messages.starts_with("Transaction caused an exception")); for (auto const& m : expect_logs) { - if (sink.messages().str().find(m) == std::string::npos) + if (messages.find(m) == std::string::npos) { // uncomment if you want to log the invariant failure // message log << " --> " << m << std::endl; @@ -1435,6 +1436,127 @@ class Invariants_test : public beast::unit_test::suite {tecINVARIANT_FAILED, tecINVARIANT_FAILED}); } + void + testValidPseudoAccounts() + { + testcase << "valid pseudo accounts"; + + using namespace jtx; + + AccountID pseudoAccountID; + Preclose createPseudo = + [&, this](Account const& a, Account const& b, Env& env) { + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + + // Create vault + Vault vault{env}; + auto [tx, vKeylet] = + vault.create({.owner = a, .asset = xrpAsset}); + env(tx); + env.close(); + if (auto const vSle = env.le(vKeylet); BEAST_EXPECT(vSle)) + { + pseudoAccountID = vSle->at(sfAccount); + } + + return BEAST_EXPECT(env.le(keylet::account(pseudoAccountID))); + }; + + /* Cases to check + "pseudo-account has 0 pseudo-account fields set" + "pseudo-account has 2 pseudo-account fields set" + "pseudo-account sequence changed" + "pseudo-account flags are not set" + "pseudo-account has a regular key" + */ + struct Mod + { + std::string expectedFailure; + std::function func; + }; + auto const mods = std::to_array({ + { + "pseudo-account has 0 pseudo-account fields set", + [this](SLE::pointer& sle) { + BEAST_EXPECT(sle->at(~sfVaultID)); + sle->at(~sfVaultID) = std::nullopt; + }, + }, + { + "pseudo-account sequence changed", + [](SLE::pointer& sle) { sle->at(sfSequence) = 12345; }, + }, + { + "pseudo-account flags are not set", + [](SLE::pointer& sle) { sle->at(sfFlags) = lsfNoFreeze; }, + }, + { + "pseudo-account has a regular key", + [](SLE::pointer& sle) { + sle->at(sfRegularKey) = Account("regular").id(); + }, + }, + }); + + for (auto const& mod : mods) + { + doInvariantCheck( + {{mod.expectedFailure}}, + [&](Account const& A1, Account const&, ApplyContext& ac) { + auto sle = ac.view().peek(keylet::account(pseudoAccountID)); + if (!sle) + return false; + mod.func(sle); + ac.view().update(sle); + return true; + }, + XRPAmount{}, + STTx{ttACCOUNT_SET, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + createPseudo); + } + for (auto const pField : getPseudoAccountFields()) + { + // createPseudo creates a vault, so sfVaultID will be set, and + // setting it again will not cause an error + if (pField == &sfVaultID) + continue; + doInvariantCheck( + {{"pseudo-account has 2 pseudo-account fields set"}}, + [&](Account const& A1, Account const&, ApplyContext& ac) { + auto sle = ac.view().peek(keylet::account(pseudoAccountID)); + if (!sle) + return false; + + auto const vaultID = ~sle->at(~sfVaultID); + BEAST_EXPECT(vaultID && !sle->isFieldPresent(*pField)); + sle->setFieldH256(*pField, *vaultID); + + ac.view().update(sle); + return true; + }, + XRPAmount{}, + STTx{ttACCOUNT_SET, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + createPseudo); + } + + // Take one of the regular accounts and set the sequence to 0, which + // will make it look like a pseudo-account + doInvariantCheck( + {{"pseudo-account has 0 pseudo-account fields set"}, + {"pseudo-account sequence changed"}, + {"pseudo-account flags are not set"}}, + [&](Account const& A1, Account const&, ApplyContext& ac) { + auto sle = ac.view().peek(keylet::account(A1.id())); + if (!sle) + return false; + sle->at(sfSequence) = 0; + ac.view().update(sle); + return true; + }); + } + void testPermissionedDEX() { @@ -1622,10 +1744,12 @@ public: testValidNewAccountRoot(); testNFTokenPageInvariants(); testPermissionedDomainInvariants(); + testValidPseudoAccounts(); testPermissionedDEX(); } }; BEAST_DEFINE_TESTSUITE(Invariants, app, ripple); +} // namespace test } // namespace ripple diff --git a/src/test/app/LPTokenTransfer_test.cpp b/src/test/app/LPTokenTransfer_test.cpp index e95e974547..eccd864e71 100644 --- a/src/test/app/LPTokenTransfer_test.cpp +++ b/src/test/app/LPTokenTransfer_test.cpp @@ -287,11 +287,11 @@ class LPTokenTransfer_test : public jtx::AMMTest // with fixFrozenLPTokenTransfer enabled, alice's offer can no // longer cross with carol's offer BEAST_EXPECT( - expectLine(env, alice, STAmount{token1, 10'000'000}) && - expectLine(env, alice, STAmount{token2, 10'000'000})); + expectHolding(env, alice, STAmount{token1, 10'000'000}) && + expectHolding(env, alice, STAmount{token2, 10'000'000})); BEAST_EXPECT( - expectLine(env, carol, STAmount{token2, 10'000'000}) && - expectLine(env, carol, STAmount{token1, 10'000'000})); + expectHolding(env, carol, STAmount{token2, 10'000'000}) && + expectHolding(env, carol, STAmount{token1, 10'000'000})); BEAST_EXPECT( expectOffers(env, alice, 1) && expectOffers(env, carol, 0)); } @@ -300,11 +300,11 @@ class LPTokenTransfer_test : public jtx::AMMTest // alice's offer still crosses with carol's offer despite carol's // token1 is frozen BEAST_EXPECT( - expectLine(env, alice, STAmount{token1, 10'000'100}) && - expectLine(env, alice, STAmount{token2, 9'999'900})); + expectHolding(env, alice, STAmount{token1, 10'000'100}) && + expectHolding(env, alice, STAmount{token2, 9'999'900})); BEAST_EXPECT( - expectLine(env, carol, STAmount{token2, 10'000'100}) && - expectLine(env, carol, STAmount{token1, 9'999'900})); + expectHolding(env, carol, STAmount{token2, 10'000'100}) && + expectHolding(env, carol, STAmount{token1, 9'999'900})); BEAST_EXPECT( expectOffers(env, alice, 0) && expectOffers(env, carol, 0)); } diff --git a/src/test/app/LedgerHistory_test.cpp b/src/test/app/LedgerHistory_test.cpp index 7b1910bd4d..1d440f6420 100644 --- a/src/test/app/LedgerHistory_test.cpp +++ b/src/test/app/LedgerHistory_test.cpp @@ -23,10 +23,10 @@ #include #include #include -#include #include #include +#include #include #include diff --git a/src/test/app/LoadFeeTrack_test.cpp b/src/test/app/LoadFeeTrack_test.cpp index 8a88e0273f..80110b073d 100644 --- a/src/test/app/LoadFeeTrack_test.cpp +++ b/src/test/app/LoadFeeTrack_test.cpp @@ -19,9 +19,9 @@ #include #include -#include #include +#include namespace ripple { diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index 6470962f2f..e9740e67de 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -589,7 +589,8 @@ class MPToken_test : public beast::unit_test::suite .flags = 0x00000008, .err = temINVALID_FLAG}); - if (!features[featureSingleAssetVault]) + if (!features[featureSingleAssetVault] && + !features[featureDynamicMPT]) { // test invalid flags - nothing is being changed mptAlice.set( @@ -623,7 +624,8 @@ class MPToken_test : public beast::unit_test::suite .flags = 0x00000000, .err = temMALFORMED}); - if (!features[featurePermissionedDomains]) + if (!features[featurePermissionedDomains] || + !features[featureSingleAssetVault]) { // cannot set DomainID since PD is not enabled mptAlice.set( @@ -631,7 +633,7 @@ class MPToken_test : public beast::unit_test::suite .domainID = uint256(42), .err = temDISABLED}); } - else + else if (features[featureSingleAssetVault]) { // cannot set DomainID since Holder is set mptAlice.set( @@ -2738,6 +2740,891 @@ class MPToken_test : public beast::unit_test::suite } } + void + testInvalidCreateDynamic(FeatureBitset features) + { + testcase("invalid MPTokenIssuanceCreate for DynamicMPT"); + + using namespace test::jtx; + Account const alice("alice"); + + // Can not provide MutableFlags when DynamicMPT amendment is not enabled + { + Env env{*this, features - featureDynamicMPT}; + MPTTester mptAlice(env, alice); + mptAlice.create( + {.ownerCount = 0, .mutableFlags = 2, .err = temDISABLED}); + mptAlice.create( + {.ownerCount = 0, .mutableFlags = 0, .err = temDISABLED}); + } + + // MutableFlags contains invalid values + { + Env env{*this, features}; + MPTTester mptAlice(env, alice); + + // Value 1 is reserved for MPT lock. + mptAlice.create( + {.ownerCount = 0, .mutableFlags = 1, .err = temINVALID_FLAG}); + mptAlice.create( + {.ownerCount = 0, .mutableFlags = 17, .err = temINVALID_FLAG}); + mptAlice.create( + {.ownerCount = 0, + .mutableFlags = 65535, + .err = temINVALID_FLAG}); + + // MutableFlags can not be 0 + mptAlice.create( + {.ownerCount = 0, .mutableFlags = 0, .err = temINVALID_FLAG}); + } + } + + void + testInvalidSetDynamic(FeatureBitset features) + { + testcase("invalid MPTokenIssuanceSet for DynamicMPT"); + + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + + // Can not provide MutableFlags, MPTokenMetadata or TransferFee when + // DynamicMPT amendment is not enabled + { + Env env{*this, features - featureDynamicMPT}; + MPTTester mptAlice(env, alice, {.holders = {bob}}); + auto const mptID = makeMptID(env.seq(alice), alice); + + // MutableFlags is not allowed when DynamicMPT is not enabled + mptAlice.set( + {.account = alice, + .id = mptID, + .mutableFlags = 2, + .err = temDISABLED}); + mptAlice.set( + {.account = alice, + .id = mptID, + .mutableFlags = 0, + .err = temDISABLED}); + + // MPTokenMetadata is not allowed when DynamicMPT is not enabled + mptAlice.set( + {.account = alice, + .id = mptID, + .metadata = "test", + .err = temDISABLED}); + mptAlice.set( + {.account = alice, + .id = mptID, + .metadata = "", + .err = temDISABLED}); + + // TransferFee is not allowed when DynamicMPT is not enabled + mptAlice.set( + {.account = alice, + .id = mptID, + .transferFee = 100, + .err = temDISABLED}); + mptAlice.set( + {.account = alice, + .id = mptID, + .transferFee = 0, + .err = temDISABLED}); + } + + // Can not provide holder when MutableFlags, MPTokenMetadata or + // TransferFee is present + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {bob}}); + auto const mptID = makeMptID(env.seq(alice), alice); + + // Holder is not allowed when MutableFlags is present + mptAlice.set( + {.account = alice, + .holder = bob, + .id = mptID, + .mutableFlags = 2, + .err = temMALFORMED}); + + // Holder is not allowed when MPTokenMetadata is present + mptAlice.set( + {.account = alice, + .holder = bob, + .id = mptID, + .metadata = "test", + .err = temMALFORMED}); + + // Holder is not allowed when TransferFee is present + mptAlice.set( + {.account = alice, + .holder = bob, + .id = mptID, + .transferFee = 100, + .err = temMALFORMED}); + } + + // Can not set Flags when MutableFlags, MPTokenMetadata or + // TransferFee is present + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {bob}}); + mptAlice.create( + {.ownerCount = 1, + .mutableFlags = tmfMPTCanMutateMetadata | + tmfMPTCanMutateCanLock | tmfMPTCanMutateTransferFee}); + + // Setting flags is not allowed when MutableFlags is present + mptAlice.set( + {.account = alice, + .flags = tfMPTCanLock, + .mutableFlags = 2, + .err = temMALFORMED}); + + // Setting flags is not allowed when MPTokenMetadata is present + mptAlice.set( + {.account = alice, + .flags = tfMPTCanLock, + .metadata = "test", + .err = temMALFORMED}); + + // setting flags is not allowed when TransferFee is present + mptAlice.set( + {.account = alice, + .flags = tfMPTCanLock, + .transferFee = 100, + .err = temMALFORMED}); + } + + // Flags being 0 or tfFullyCanonicalSig is fine + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create( + {.transferFee = 10, + .ownerCount = 1, + .flags = tfMPTCanTransfer, + .mutableFlags = + tmfMPTCanMutateTransferFee | tmfMPTCanMutateMetadata}); + + mptAlice.set( + {.account = alice, + .flags = 0, + .transferFee = 100, + .metadata = "test"}); + mptAlice.set( + {.account = alice, + .flags = tfFullyCanonicalSig, + .transferFee = 200, + .metadata = "test2"}); + } + + // Invalid MutableFlags + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {bob}}); + auto const mptID = makeMptID(env.seq(alice), alice); + + for (auto const flags : {10000, 0, 5000}) + { + mptAlice.set( + {.account = alice, + .id = mptID, + .mutableFlags = flags, + .err = temINVALID_FLAG}); + } + } + + // Can not set and clear the same mutable flag + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {bob}}); + auto const mptID = makeMptID(env.seq(alice), alice); + + auto const flagCombinations = { + tmfMPTSetCanLock | tmfMPTClearCanLock, + tmfMPTSetRequireAuth | tmfMPTClearRequireAuth, + tmfMPTSetCanEscrow | tmfMPTClearCanEscrow, + tmfMPTSetCanTrade | tmfMPTClearCanTrade, + tmfMPTSetCanTransfer | tmfMPTClearCanTransfer, + tmfMPTSetCanClawback | tmfMPTClearCanClawback, + tmfMPTSetCanLock | tmfMPTClearCanLock | tmfMPTClearCanTrade, + tmfMPTSetCanTransfer | tmfMPTClearCanTransfer | + tmfMPTSetCanEscrow | tmfMPTClearCanClawback}; + + for (auto const& mutableFlags : flagCombinations) + { + mptAlice.set( + {.account = alice, + .id = mptID, + .mutableFlags = mutableFlags, + .err = temINVALID_FLAG}); + } + } + + // Can not mutate flag which is not mutable + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({.ownerCount = 1}); + + auto const mutableFlags = { + tmfMPTSetCanLock, + tmfMPTClearCanLock, + tmfMPTSetRequireAuth, + tmfMPTClearRequireAuth, + tmfMPTSetCanEscrow, + tmfMPTClearCanEscrow, + tmfMPTSetCanTrade, + tmfMPTClearCanTrade, + tmfMPTSetCanTransfer, + tmfMPTClearCanTransfer, + tmfMPTSetCanClawback, + tmfMPTClearCanClawback}; + + for (auto const& mutableFlag : mutableFlags) + { + mptAlice.set( + {.account = alice, + .mutableFlags = mutableFlag, + .err = tecNO_PERMISSION}); + } + } + + // Metadata exceeding max length + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create( + {.ownerCount = 1, .mutableFlags = tmfMPTCanMutateMetadata}); + + std::string metadata(maxMPTokenMetadataLength + 1, 'a'); + mptAlice.set( + {.account = alice, .metadata = metadata, .err = temMALFORMED}); + } + + // Can not mutate metadata when it is not mutable + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({.ownerCount = 1}); + mptAlice.set( + {.account = alice, + .metadata = "test", + .err = tecNO_PERMISSION}); + } + + // Transfer fee exceeding the max value + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {bob}}); + auto const mptID = makeMptID(env.seq(alice), alice); + + mptAlice.create( + {.ownerCount = 1, .mutableFlags = tmfMPTCanMutateTransferFee}); + + mptAlice.set( + {.account = alice, + .id = mptID, + .transferFee = maxTransferFee + 1, + .err = temBAD_TRANSFER_FEE}); + } + + // Test setting non-zero transfer fee and clearing MPTCanTransfer at the + // same time + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create( + {.transferFee = 100, + .ownerCount = 1, + .flags = tfMPTCanTransfer, + .mutableFlags = + tmfMPTCanMutateTransferFee | tmfMPTCanMutateCanTransfer}); + + // Can not set non-zero transfer fee and clear MPTCanTransfer at the + // same time + mptAlice.set( + {.account = alice, + .mutableFlags = tmfMPTClearCanTransfer, + .transferFee = 1, + .err = temMALFORMED}); + + // Can set transfer fee to zero and clear MPTCanTransfer at the same + // time. tfMPTCanTransfer will be cleared and TransferFee field will + // be removed. + mptAlice.set( + {.account = alice, + .mutableFlags = tmfMPTClearCanTransfer, + .transferFee = 0}); + BEAST_EXPECT(!mptAlice.isTransferFeePresent()); + } + + // Can not set non-zero transfer fee when MPTCanTransfer is not set + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create( + {.ownerCount = 1, + .mutableFlags = + tmfMPTCanMutateTransferFee | tmfMPTCanMutateCanTransfer}); + + mptAlice.set( + {.account = alice, + .transferFee = 100, + .err = tecNO_PERMISSION}); + + // Can not set transfer fee even when trying to set MPTCanTransfer + // at the same time. MPTCanTransfer must be set first, then transfer + // fee can be set in a separate transaction. + mptAlice.set( + {.account = alice, + .mutableFlags = tmfMPTSetCanTransfer, + .transferFee = 100, + .err = tecNO_PERMISSION}); + } + + // Can not mutate transfer fee when it is not mutable + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create( + {.transferFee = 10, + .ownerCount = 1, + .flags = tfMPTCanTransfer}); + + mptAlice.set( + {.account = alice, + .transferFee = 100, + .err = tecNO_PERMISSION}); + + mptAlice.set( + {.account = alice, .transferFee = 0, .err = tecNO_PERMISSION}); + } + + // Set some flags mutable. Can not mutate the others + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create( + {.ownerCount = 1, + .mutableFlags = tmfMPTCanMutateCanTrade | + tmfMPTCanMutateCanTransfer | tmfMPTCanMutateMetadata}); + + // Can not mutate transfer fee + mptAlice.set( + {.account = alice, + .transferFee = 100, + .err = tecNO_PERMISSION}); + + auto const invalidFlags = { + tmfMPTSetCanLock, + tmfMPTClearCanLock, + tmfMPTSetRequireAuth, + tmfMPTClearRequireAuth, + tmfMPTSetCanEscrow, + tmfMPTClearCanEscrow, + tmfMPTSetCanClawback, + tmfMPTClearCanClawback}; + + // Can not mutate flags which are not mutable + for (auto const& mutableFlag : invalidFlags) + { + mptAlice.set( + {.account = alice, + .mutableFlags = mutableFlag, + .err = tecNO_PERMISSION}); + } + + // Can mutate MPTCanTrade + mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanTrade}); + mptAlice.set( + {.account = alice, .mutableFlags = tmfMPTClearCanTrade}); + + // Can mutate MPTCanTransfer + mptAlice.set( + {.account = alice, .mutableFlags = tmfMPTSetCanTransfer}); + mptAlice.set( + {.account = alice, .mutableFlags = tmfMPTClearCanTransfer}); + + // Can mutate metadata + mptAlice.set({.account = alice, .metadata = "test"}); + mptAlice.set({.account = alice, .metadata = ""}); + } + } + + void + testMutateMPT(FeatureBitset features) + { + testcase("Mutate MPT"); + using namespace test::jtx; + + Account const alice("alice"); + + // Mutate metadata + { + Env env{*this, features}; + MPTTester mptAlice(env, alice); + mptAlice.create( + {.metadata = "test", + .ownerCount = 1, + .mutableFlags = tmfMPTCanMutateMetadata}); + + std::vector metadatas = { + "mutate metadata", + "mutate metadata 2", + "mutate metadata 3", + "mutate metadata 3", + "test", + "mutate metadata"}; + + for (auto const& metadata : metadatas) + { + mptAlice.set({.account = alice, .metadata = metadata}); + BEAST_EXPECT(mptAlice.checkMetadata(metadata)); + } + + // Metadata being empty will remove the field + mptAlice.set({.account = alice, .metadata = ""}); + BEAST_EXPECT(!mptAlice.isMetadataPresent()); + } + + // Mutate transfer fee + { + Env env{*this, features}; + MPTTester mptAlice(env, alice); + mptAlice.create( + {.transferFee = 100, + .metadata = "test", + .ownerCount = 1, + .flags = tfMPTCanTransfer, + .mutableFlags = tmfMPTCanMutateTransferFee}); + + for (std::uint16_t const fee : std::initializer_list{ + 1, 10, 100, 200, 500, 1000, maxTransferFee}) + { + mptAlice.set({.account = alice, .transferFee = fee}); + BEAST_EXPECT(mptAlice.checkTransferFee(fee)); + } + + // Setting TransferFee to zero will remove the field + mptAlice.set({.account = alice, .transferFee = 0}); + BEAST_EXPECT(!mptAlice.isTransferFeePresent()); + + // Set transfer fee again + mptAlice.set({.account = alice, .transferFee = 10}); + BEAST_EXPECT(mptAlice.checkTransferFee(10)); + } + + // Test flag toggling + { + auto testFlagToggle = [&](std::uint32_t createFlags, + std::uint32_t setFlags, + std::uint32_t clearFlags) { + Env env{*this, features}; + MPTTester mptAlice(env, alice); + + // Create the MPT object with the specified initial flags + mptAlice.create( + {.metadata = "test", + .ownerCount = 1, + .mutableFlags = createFlags}); + + // Set and clear the flag multiple times + mptAlice.set({.account = alice, .mutableFlags = setFlags}); + mptAlice.set({.account = alice, .mutableFlags = clearFlags}); + mptAlice.set({.account = alice, .mutableFlags = clearFlags}); + mptAlice.set({.account = alice, .mutableFlags = setFlags}); + mptAlice.set({.account = alice, .mutableFlags = setFlags}); + mptAlice.set({.account = alice, .mutableFlags = clearFlags}); + mptAlice.set({.account = alice, .mutableFlags = setFlags}); + mptAlice.set({.account = alice, .mutableFlags = clearFlags}); + }; + + testFlagToggle( + tmfMPTCanMutateCanLock, tfMPTCanLock, tmfMPTClearCanLock); + testFlagToggle( + tmfMPTCanMutateRequireAuth, + tmfMPTSetRequireAuth, + tmfMPTClearRequireAuth); + testFlagToggle( + tmfMPTCanMutateCanEscrow, + tmfMPTSetCanEscrow, + tmfMPTClearCanEscrow); + testFlagToggle( + tmfMPTCanMutateCanTrade, + tmfMPTSetCanTrade, + tmfMPTClearCanTrade); + testFlagToggle( + tmfMPTCanMutateCanTransfer, + tmfMPTSetCanTransfer, + tmfMPTClearCanTransfer); + testFlagToggle( + tmfMPTCanMutateCanClawback, + tmfMPTSetCanClawback, + tmfMPTClearCanClawback); + } + } + + void + testMutateCanLock(FeatureBitset features) + { + testcase("Mutate MPTCanLock"); + using namespace test::jtx; + + Account const alice("alice"); + Account const bob("bob"); + + // Individual lock + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {bob}}); + mptAlice.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanLock | tfMPTCanTransfer, + .mutableFlags = tmfMPTCanMutateCanLock | + tmfMPTCanMutateCanTrade | tmfMPTCanMutateTransferFee}); + mptAlice.authorize({.account = bob, .holderCount = 1}); + + // Lock bob's mptoken + mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock}); + + // Can mutate the mutable flags and fields + mptAlice.set( + {.account = alice, .mutableFlags = tmfMPTClearCanLock}); + mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanLock}); + mptAlice.set( + {.account = alice, .mutableFlags = tmfMPTClearCanLock}); + mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanTrade}); + mptAlice.set( + {.account = alice, .mutableFlags = tmfMPTClearCanTrade}); + mptAlice.set({.account = alice, .transferFee = 200}); + } + + // Global lock + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {bob}}); + mptAlice.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanLock, + .mutableFlags = tmfMPTCanMutateCanLock | + tmfMPTCanMutateCanClawback | tmfMPTCanMutateMetadata}); + mptAlice.authorize({.account = bob, .holderCount = 1}); + + // Lock issuance + mptAlice.set({.account = alice, .flags = tfMPTLock}); + + // Can mutate the mutable flags and fields + mptAlice.set( + {.account = alice, .mutableFlags = tmfMPTClearCanLock}); + mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanLock}); + mptAlice.set( + {.account = alice, .mutableFlags = tmfMPTClearCanLock}); + mptAlice.set( + {.account = alice, .mutableFlags = tmfMPTSetCanClawback}); + mptAlice.set( + {.account = alice, .mutableFlags = tmfMPTClearCanClawback}); + mptAlice.set({.account = alice, .metadata = "mutate"}); + } + + // Test lock and unlock after mutating MPTCanLock + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {bob}}); + mptAlice.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanLock, + .mutableFlags = tmfMPTCanMutateCanLock | + tmfMPTCanMutateCanClawback | tmfMPTCanMutateMetadata}); + mptAlice.authorize({.account = bob, .holderCount = 1}); + + // Can lock and unlock + mptAlice.set({.account = alice, .flags = tfMPTLock}); + mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock}); + mptAlice.set({.account = alice, .flags = tfMPTUnlock}); + mptAlice.set( + {.account = alice, .holder = bob, .flags = tfMPTUnlock}); + + // Clear lsfMPTCanLock + mptAlice.set( + {.account = alice, .mutableFlags = tmfMPTClearCanLock}); + + // Can not lock or unlock + mptAlice.set( + {.account = alice, + .flags = tfMPTLock, + .err = tecNO_PERMISSION}); + mptAlice.set( + {.account = alice, + .flags = tfMPTUnlock, + .err = tecNO_PERMISSION}); + mptAlice.set( + {.account = alice, + .holder = bob, + .flags = tfMPTLock, + .err = tecNO_PERMISSION}); + mptAlice.set( + {.account = alice, + .holder = bob, + .flags = tfMPTUnlock, + .err = tecNO_PERMISSION}); + + // Set MPTCanLock again + mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanLock}); + + // Can lock and unlock again + mptAlice.set({.account = alice, .flags = tfMPTLock}); + mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock}); + mptAlice.set({.account = alice, .flags = tfMPTUnlock}); + mptAlice.set( + {.account = alice, .holder = bob, .flags = tfMPTUnlock}); + } + } + + void + testMutateRequireAuth(FeatureBitset features) + { + testcase("Mutate MPTRequireAuth"); + using namespace test::jtx; + + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + + MPTTester mptAlice(env, alice, {.holders = {bob}}); + mptAlice.create( + {.ownerCount = 1, + .flags = tfMPTRequireAuth, + .mutableFlags = tmfMPTCanMutateRequireAuth}); + + mptAlice.authorize({.account = bob}); + mptAlice.authorize({.account = alice, .holder = bob}); + + // Pay to bob + mptAlice.pay(alice, bob, 1000); + + // Unauthorize bob + mptAlice.authorize( + {.account = alice, .holder = bob, .flags = tfMPTUnauthorize}); + + // Can not pay to bob + mptAlice.pay(bob, alice, 100, tecNO_AUTH); + + // Clear RequireAuth + mptAlice.set( + {.account = alice, .mutableFlags = tmfMPTClearRequireAuth}); + + // Can pay to bob + mptAlice.pay(alice, bob, 1000); + + // Set RequireAuth again + mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetRequireAuth}); + + // Can not pay to bob since he is not authorized + mptAlice.pay(bob, alice, 100, tecNO_AUTH); + + // Authorize bob again + mptAlice.authorize({.account = alice, .holder = bob}); + + // Can pay to bob again + mptAlice.pay(alice, bob, 100); + } + + void + testMutateCanEscrow(FeatureBitset features) + { + testcase("Mutate MPTCanEscrow"); + using namespace test::jtx; + using namespace std::literals; + + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + + MPTTester mptAlice(env, alice, {.holders = {carol, bob}}); + mptAlice.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer, + .mutableFlags = tmfMPTCanMutateCanEscrow}); + mptAlice.authorize({.account = carol}); + mptAlice.authorize({.account = bob}); + + auto const MPT = mptAlice["MPT"]; + env(pay(alice, carol, MPT(10'000))); + env(pay(alice, bob, MPT(10'000))); + env.close(); + + // MPTCanEscrow is not enabled + env(escrow::create(carol, bob, MPT(3)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_PERMISSION)); + + // MPTCanEscrow is enabled now + mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanEscrow}); + env(escrow::create(carol, bob, MPT(3)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + + // Clear MPTCanEscrow + mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanEscrow}); + env(escrow::create(carol, bob, MPT(3)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_PERMISSION)); + } + + void + testMutateCanTransfer(FeatureBitset features) + { + testcase("Mutate MPTCanTransfer"); + + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + + { + Env env{*this, features}; + + MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); + mptAlice.create( + {.ownerCount = 1, + .mutableFlags = + tmfMPTCanMutateCanTransfer | tmfMPTCanMutateTransferFee}); + + mptAlice.authorize({.account = bob}); + mptAlice.authorize({.account = carol}); + + // Pay to bob + mptAlice.pay(alice, bob, 1000); + + // Bob can not pay carol since MPTCanTransfer is not set + mptAlice.pay(bob, carol, 50, tecNO_AUTH); + + // Can not set non-zero transfer fee when MPTCanTransfer is not set + mptAlice.set( + {.account = alice, + .transferFee = 100, + .err = tecNO_PERMISSION}); + + // Can not set non-zero transfer fee even when trying to set + // MPTCanTransfer at the same time + mptAlice.set( + {.account = alice, + .mutableFlags = tmfMPTSetCanTransfer, + .transferFee = 100, + .err = tecNO_PERMISSION}); + + // Alice sets MPTCanTransfer + mptAlice.set( + {.account = alice, .mutableFlags = tmfMPTSetCanTransfer}); + + // Can set transfer fee now + BEAST_EXPECT(!mptAlice.isTransferFeePresent()); + mptAlice.set({.account = alice, .transferFee = 100}); + BEAST_EXPECT(mptAlice.isTransferFeePresent()); + + // Bob can pay carol + mptAlice.pay(bob, carol, 50); + + // Alice clears MPTCanTransfer + mptAlice.set( + {.account = alice, .mutableFlags = tmfMPTClearCanTransfer}); + + // TransferFee field is removed when MPTCanTransfer is cleared + BEAST_EXPECT(!mptAlice.isTransferFeePresent()); + + // Bob can not pay + mptAlice.pay(bob, carol, 50, tecNO_AUTH); + } + + // Can set transfer fee to zero when MPTCanTransfer is not set, but + // tmfMPTCanMutateTransferFee is set. + { + Env env{*this, features}; + + MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); + mptAlice.create( + {.transferFee = 100, + .ownerCount = 1, + .flags = tfMPTCanTransfer, + .mutableFlags = + tmfMPTCanMutateTransferFee | tmfMPTCanMutateCanTransfer}); + + BEAST_EXPECT(mptAlice.checkTransferFee(100)); + + // Clear MPTCanTransfer and transfer fee is removed + mptAlice.set( + {.account = alice, .mutableFlags = tmfMPTClearCanTransfer}); + BEAST_EXPECT(!mptAlice.isTransferFeePresent()); + + // Can still set transfer fee to zero, although it is already zero + mptAlice.set({.account = alice, .transferFee = 0}); + + // TransferFee field is still not present + BEAST_EXPECT(!mptAlice.isTransferFeePresent()); + } + } + + void + testMutateCanClawback(FeatureBitset features) + { + testcase("Mutate MPTCanClawback"); + + using namespace test::jtx; + Env env(*this, features); + Account const alice{"alice"}; + Account const bob{"bob"}; + + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create( + {.ownerCount = 1, + .holderCount = 0, + .mutableFlags = tmfMPTCanMutateCanClawback}); + + // Bob creates an MPToken + mptAlice.authorize({.account = bob}); + + // Alice pays bob 100 tokens + mptAlice.pay(alice, bob, 100); + + // MPTCanClawback is not enabled + mptAlice.claw(alice, bob, 1, tecNO_PERMISSION); + + // Enable MPTCanClawback + mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanClawback}); + + // Can clawback now + mptAlice.claw(alice, bob, 1); + + // Clear MPTCanClawback + mptAlice.set( + {.account = alice, .mutableFlags = tmfMPTClearCanClawback}); + + // Can not clawback + mptAlice.claw(alice, bob, 1, tecNO_PERMISSION); + } + public: void run() override @@ -2747,39 +3634,39 @@ public: // MPTokenIssuanceCreate testCreateValidation(all - featureSingleAssetVault); - testCreateValidation( - (all | featureSingleAssetVault) - featurePermissionedDomains); - testCreateValidation(all | featureSingleAssetVault); + testCreateValidation(all - featurePermissionedDomains); + testCreateValidation(all); testCreateEnabled(all - featureSingleAssetVault); - testCreateEnabled(all | featureSingleAssetVault); + testCreateEnabled(all); // MPTokenIssuanceDestroy testDestroyValidation(all - featureSingleAssetVault); - testDestroyValidation(all | featureSingleAssetVault); + testDestroyValidation(all); testDestroyEnabled(all - featureSingleAssetVault); - testDestroyEnabled(all | featureSingleAssetVault); + testDestroyEnabled(all); // MPTokenAuthorize testAuthorizeValidation(all - featureSingleAssetVault); - testAuthorizeValidation(all | featureSingleAssetVault); + testAuthorizeValidation(all); testAuthorizeEnabled(all - featureSingleAssetVault); - testAuthorizeEnabled(all | featureSingleAssetVault); + testAuthorizeEnabled(all); // MPTokenIssuanceSet + testSetValidation(all - featureSingleAssetVault - featureDynamicMPT); testSetValidation(all - featureSingleAssetVault); - testSetValidation( - (all | featureSingleAssetVault) - featurePermissionedDomains); - testSetValidation(all | featureSingleAssetVault); + testSetValidation(all - featureDynamicMPT); + testSetValidation(all - featurePermissionedDomains); + testSetValidation(all); testSetEnabled(all - featureSingleAssetVault); - testSetEnabled(all | featureSingleAssetVault); + testSetEnabled(all); // MPT clawback testClawbackValidation(all); testClawback(all); // Test Direct Payment - testPayment(all | featureSingleAssetVault); + testPayment(all); testDepositPreauth(all); testDepositPreauth(all - featureCredentials); @@ -2794,6 +3681,16 @@ public: // Test helpers testHelperFunctions(); + + // Dynamic MPT + testInvalidCreateDynamic(all); + testInvalidSetDynamic(all); + testMutateMPT(all); + testMutateCanLock(all); + testMutateRequireAuth(all); + testMutateCanEscrow(all); + testMutateCanTransfer(all); + testMutateCanClawback(all); } }; diff --git a/src/test/app/MultiSign_test.cpp b/src/test/app/MultiSign_test.cpp index 571ec33417..776e163cd4 100644 --- a/src/test/app/MultiSign_test.cpp +++ b/src/test/app/MultiSign_test.cpp @@ -63,7 +63,7 @@ class MultiSign_test : public beast::unit_test::suite public: void - test_noReserve(FeatureBitset features) + testNoReserve(FeatureBitset features) { testcase("No Reserve"); @@ -133,7 +133,7 @@ public: } void - test_signerListSet(FeatureBitset features) + testSignerListSet(FeatureBitset features) { testcase("SignerListSet"); @@ -215,7 +215,7 @@ public: } void - test_phantomSigners(FeatureBitset features) + testPhantomSigners(FeatureBitset features) { testcase("Phantom Signers"); @@ -282,7 +282,7 @@ public: } void - test_fee(FeatureBitset features) + testFee(FeatureBitset features) { testcase("Fee"); @@ -346,7 +346,7 @@ public: } void - test_misorderedSigners(FeatureBitset features) + testMisorderedSigners(FeatureBitset features) { testcase("Misordered Signers"); @@ -374,7 +374,7 @@ public: } void - test_masterSigners(FeatureBitset features) + testMasterSigners(FeatureBitset features) { testcase("Master Signers"); @@ -429,7 +429,7 @@ public: } void - test_regularSigners(FeatureBitset features) + testRegularSigners(FeatureBitset features) { testcase("Regular Signers"); @@ -494,7 +494,7 @@ public: } void - test_regularSignersUsingSubmitMulti(FeatureBitset features) + testRegularSignersUsingSubmitMulti(FeatureBitset features) { testcase("Regular Signers Using submit_multisigned"); @@ -734,7 +734,7 @@ public: } void - test_heterogeneousSigners(FeatureBitset features) + testHeterogeneousSigners(FeatureBitset features) { testcase("Heterogenious Signers"); @@ -881,7 +881,7 @@ public: // We want to always leave an account signable. Make sure the that we // disallow removing the last way a transaction may be signed. void - test_keyDisable(FeatureBitset features) + testKeyDisable(FeatureBitset features) { testcase("Key Disable"); @@ -963,7 +963,7 @@ public: // Verify that the first regular key can be made for free using the // master key, but not when multisigning. void - test_regKey(FeatureBitset features) + testRegKey(FeatureBitset features) { testcase("Regular Key"); @@ -1000,7 +1000,7 @@ public: // See if every kind of transaction can be successfully multi-signed. void - test_txTypes(FeatureBitset features) + testTxTypes(FeatureBitset features) { testcase("Transaction Types"); @@ -1089,7 +1089,7 @@ public: } void - test_badSignatureText(FeatureBitset features) + testBadSignatureText(FeatureBitset features) { testcase("Bad Signature Text"); @@ -1285,7 +1285,7 @@ public: } void - test_noMultiSigners(FeatureBitset features) + testNoMultiSigners(FeatureBitset features) { testcase("No Multisigners"); @@ -1304,7 +1304,7 @@ public: } void - test_multisigningMultisigner(FeatureBitset features) + testMultisigningMultisigner(FeatureBitset features) { testcase("Multisigning multisigner"); @@ -1381,7 +1381,7 @@ public: } void - test_signForHash(FeatureBitset features) + testSignForHash(FeatureBitset features) { testcase("sign_for Hash"); @@ -1464,7 +1464,7 @@ public: } void - test_amendmentTransition() + testAmendmentTransition() { testcase("Amendment Transition"); @@ -1559,7 +1559,7 @@ public: } void - test_signersWithTickets(FeatureBitset features) + testSignersWithTickets(FeatureBitset features) { testcase("Signers With Tickets"); @@ -1600,7 +1600,7 @@ public: } void - test_signersWithTags(FeatureBitset features) + testSignersWithTags(FeatureBitset features) { if (!features[featureExpandedSignerList]) return; @@ -1680,7 +1680,7 @@ public: } void - test_signerListSetFlags(FeatureBitset features) + testSignerListSetFlags(FeatureBitset features) { using namespace test::jtx; @@ -1702,27 +1702,57 @@ public: env.close(); } + void + testSignerListObject(FeatureBitset features) + { + testcase("SignerList Object"); + + // Verify that the SignerList object is created correctly. + using namespace jtx; + Env env{*this, features}; + Account const alice{"alice", KeyType::ed25519}; + env.fund(XRP(1000), alice); + env.close(); + + // Attach phantom signers to alice. + env(signers(alice, 1, {{bogie, 1}, {demon, 1}})); + env.close(); + + // Verify that the SignerList object was created correctly. + auto const& sle = env.le(keylet::signers(alice.id())); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->getFieldArray(sfSignerEntries).size() == 2); + if (features[fixIncludeKeyletFields]) + { + BEAST_EXPECT((*sle)[sfOwner] == alice.id()); + } + else + { + BEAST_EXPECT(!sle->isFieldPresent(sfOwner)); + } + } + void testAll(FeatureBitset features) { - test_noReserve(features); - test_signerListSet(features); - test_phantomSigners(features); - test_fee(features); - test_misorderedSigners(features); - test_masterSigners(features); - test_regularSigners(features); - test_regularSignersUsingSubmitMulti(features); - test_heterogeneousSigners(features); - test_keyDisable(features); - test_regKey(features); - test_txTypes(features); - test_badSignatureText(features); - test_noMultiSigners(features); - test_multisigningMultisigner(features); - test_signForHash(features); - test_signersWithTickets(features); - test_signersWithTags(features); + testNoReserve(features); + testSignerListSet(features); + testPhantomSigners(features); + testFee(features); + testMisorderedSigners(features); + testMasterSigners(features); + testRegularSigners(features); + testRegularSignersUsingSubmitMulti(features); + testHeterogeneousSigners(features); + testKeyDisable(features); + testRegKey(features); + testTxTypes(features); + testBadSignatureText(features); + testNoMultiSigners(features); + testMultisigningMultisigner(features); + testSignForHash(features); + testSignersWithTickets(features); + testSignersWithTags(features); } void @@ -1739,10 +1769,13 @@ public: testAll(all - featureExpandedSignerList); testAll(all); - test_signerListSetFlags(all - fixInvalidTxFlags); - test_signerListSetFlags(all); + testSignerListSetFlags(all - fixInvalidTxFlags); + testSignerListSetFlags(all); - test_amendmentTransition(); + testSignerListObject(all - fixIncludeKeyletFields); + testSignerListObject(all); + + testAmendmentTransition(); } }; diff --git a/src/test/app/NetworkOPs_test.cpp b/src/test/app/NetworkOPs_test.cpp new file mode 100644 index 0000000000..edea55105b --- /dev/null +++ b/src/test/app/NetworkOPs_test.cpp @@ -0,0 +1,80 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2020 Dev Null Productions + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include + +#include + +namespace ripple { +namespace test { + +class NetworkOPs_test : public beast::unit_test::suite +{ +public: + void + run() override + { + testAllBadHeldTransactions(); + } + + void + testAllBadHeldTransactions() + { + // All trasactions are already marked as SF_BAD, and we should be able + // to handle the case properly without an assertion failure + testcase("No valid transactions in batch"); + + std::string logs; + + { + using namespace jtx; + auto const alice = Account{"alice"}; + Env env{ + *this, + envconfig(), + std::make_unique(&logs), + beast::severities::kAll}; + env.memoize(env.master); + env.memoize(alice); + + auto const jtx = env.jt(ticket::create(alice, 1), seq(1), fee(10)); + + auto transacionId = jtx.stx->getTransactionID(); + env.app().getHashRouter().setFlags( + transacionId, HashRouterFlags::HELD); + + env(jtx, json(jss::Sequence, 1), ter(terNO_ACCOUNT)); + + env.app().getHashRouter().setFlags( + transacionId, HashRouterFlags::BAD); + + env.close(); + } + + BEAST_EXPECT( + logs.find("No transaction to process!") != std::string::npos); + } +}; + +BEAST_DEFINE_TESTSUITE(NetworkOPs, app, ripple); + +} // namespace test +} // namespace ripple diff --git a/src/test/app/Oracle_test.cpp b/src/test/app/Oracle_test.cpp index fdd7ad941e..f0cde41394 100644 --- a/src/test/app/Oracle_test.cpp +++ b/src/test/app/Oracle_test.cpp @@ -398,7 +398,7 @@ private: } void - testCreate() + testCreate(FeatureBitset features) { testcase("Create"); using namespace jtx; @@ -413,18 +413,30 @@ private: env, {.owner = owner, .series = series, .fee = baseFee}); BEAST_EXPECT(oracle.exists()); BEAST_EXPECT(ownerCount(env, owner) == (count + adj)); + auto const entry = oracle.ledgerEntry(); + BEAST_EXPECT(entry[jss::node][jss::Owner] == owner.human()); + if (features[fixIncludeKeyletFields]) + { + BEAST_EXPECT( + entry[jss::node][jss::OracleDocumentID] == + oracle.documentID()); + } + else + { + BEAST_EXPECT(!entry[jss::node].isMember(jss::OracleDocumentID)); + } BEAST_EXPECT(oracle.expectLastUpdateTime(946694810)); }; { // owner count is adjusted by 1 - Env env(*this); + Env env(*this, features); test(env, {{"XRP", "USD", 740, 1}}, 1); } { // owner count is adjusted by 2 - Env env(*this); + Env env(*this, features); test( env, {{"XRP", "USD", 740, 1}, @@ -438,7 +450,7 @@ private: { // Different owner creates a new object - Env env(*this); + Env env(*this, features); auto const baseFee = static_cast(env.current()->fees().base.drops()); Account const some("some"); @@ -864,7 +876,8 @@ public: auto const all = testable_amendments(); testInvalidSet(); testInvalidDelete(); - testCreate(); + testCreate(all); + testCreate(all - fixIncludeKeyletFields); testDelete(); testUpdate(); testAmendment(); diff --git a/src/test/app/PayChan_test.cpp b/src/test/app/PayChan_test.cpp index 3a5d3d6ff5..fe9b70cf7f 100644 --- a/src/test/app/PayChan_test.cpp +++ b/src/test/app/PayChan_test.cpp @@ -19,10 +19,10 @@ #include -#include #include #include +#include #include #include #include @@ -1852,6 +1852,14 @@ struct PayChan_test : public beast::unit_test::suite BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1); BEAST_EXPECT(!inOwnerDir(*env.current(), bob, chanSle)); BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 0); + if (features[fixIncludeKeyletFields]) + { + BEAST_EXPECT((*chanSle)[sfSequence] == env.seq(alice) - 1); + } + else + { + BEAST_EXPECT(!chanSle->isFieldPresent(sfSequence)); + } // close the channel env(claim(bob, chan), txflags(tfClose)); BEAST_EXPECT(!channelExists(*env.current(), chan)); @@ -2348,6 +2356,7 @@ public: testWithFeats(all - disallowIncoming); testWithFeats(all); testDepositAuthCreds(); + testMetaAndOwnership(all - fixIncludeKeyletFields); } }; diff --git a/src/test/app/PayStrand_test.cpp b/src/test/app/PayStrand_test.cpp index 936fe403d4..16a6861bfd 100644 --- a/src/test/app/PayStrand_test.cpp +++ b/src/test/app/PayStrand_test.cpp @@ -22,10 +22,10 @@ #include #include #include -#include #include #include +#include #include #include diff --git a/src/test/app/PermissionedDEX_test.cpp b/src/test/app/PermissionedDEX_test.cpp index 3fd3a35f45..80c75a2daf 100644 --- a/src/test/app/PermissionedDEX_test.cpp +++ b/src/test/app/PermissionedDEX_test.cpp @@ -22,11 +22,11 @@ #include #include -#include #include #include #include +#include #include #include #include diff --git a/src/test/app/TheoreticalQuality_test.cpp b/src/test/app/TheoreticalQuality_test.cpp index a8713ec69a..814e6f7136 100644 --- a/src/test/app/TheoreticalQuality_test.cpp +++ b/src/test/app/TheoreticalQuality_test.cpp @@ -24,10 +24,10 @@ #include #include #include -#include #include #include +#include #include #include diff --git a/src/test/app/ValidatorList_test.cpp b/src/test/app/ValidatorList_test.cpp index a3b62bd4f7..2b004c3b52 100644 --- a/src/test/app/ValidatorList_test.cpp +++ b/src/test/app/ValidatorList_test.cpp @@ -768,6 +768,24 @@ private: expectUntrusted(lists.at(7)); expectTrusted(lists.at(2)); + // try empty or mangled manifest + checkResult( + trustedKeys->applyLists( + "", version, {{blob7, sig7, {}}, {blob6, sig6, {}}}, siteUri), + publisherPublic, + ListDisposition::invalid, + ListDisposition::invalid); + + checkResult( + trustedKeys->applyLists( + base64_encode("not a manifest"), + version, + {{blob7, sig7, {}}, {blob6, sig6, {}}}, + siteUri), + publisherPublic, + ListDisposition::invalid, + ListDisposition::invalid); + // do not use list from untrusted publisher auto const untrustedManifest = base64_encode(makeManifestString( randomMasterKey(), diff --git a/src/test/app/ValidatorSite_test.cpp b/src/test/app/ValidatorSite_test.cpp index 579cd79a5a..cd60a5ed99 100644 --- a/src/test/app/ValidatorSite_test.cpp +++ b/src/test/app/ValidatorSite_test.cpp @@ -37,7 +37,6 @@ #include namespace ripple { -namespace test { namespace detail { constexpr char const* realValidatorContents() @@ -56,6 +55,7 @@ auto constexpr default_expires = std::chrono::seconds{3600}; auto constexpr default_effective_overlap = std::chrono::seconds{30}; } // namespace detail +namespace test { class ValidatorSite_test : public beast::unit_test::suite { private: diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 2216ff6421..159bfd0796 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -21,14 +21,13 @@ #include #include -#include -#include - #include #include #include #include #include +#include +#include #include #include #include @@ -1340,7 +1339,7 @@ class Vault_test : public beast::unit_test::suite env.close(); Vault vault{env}; - Asset asset = issuer["IOU"]; + Asset asset = issuer["IOU"].asset(); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); @@ -1359,7 +1358,7 @@ class Vault_test : public beast::unit_test::suite env.close(); Vault vault{env}; - Asset asset = issuer["IOU"]; + Asset asset = issuer["IOU"].asset(); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); env(tx, ter(terNO_RIPPLE)); @@ -1375,7 +1374,7 @@ class Vault_test : public beast::unit_test::suite env.close(); Vault vault{env}; - Asset asset = issuer["IOU"]; + Asset asset = issuer["IOU"].asset(); { auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); @@ -3166,7 +3165,7 @@ class Vault_test : public beast::unit_test::suite { using namespace test::jtx; - testcase("failed pseudo-account allocation"); + testcase("fail pseudo-account allocation"); Env env{*this, testable_amendments() | featureSingleAssetVault}; Account const owner{"owner"}; Vault vault{env}; @@ -3503,7 +3502,7 @@ class Vault_test : public beast::unit_test::suite STAmount(d.asset, Number(100, 0))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == - STAmount(d.share, Number(1000, 0))); + STAmount(d.share, Number(-1000, 0))); { testcase("Scale redeem exact"); @@ -3528,7 +3527,7 @@ class Vault_test : public beast::unit_test::suite STAmount(d.asset, Number(90, 0))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == - STAmount(d.share, Number(900, 0))); + STAmount(d.share, Number(-900, 0))); } { @@ -3563,7 +3562,7 @@ class Vault_test : public beast::unit_test::suite STAmount(d.asset, Number(900 - 25, -1))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == - STAmount(d.share, Number(900 - 25, 0))); + STAmount(d.share, -Number(900 - 25, 0))); } { @@ -3590,7 +3589,7 @@ class Vault_test : public beast::unit_test::suite STAmount(d.asset, Number(875 - 21, -1))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == - STAmount(d.share, Number(875 - 21, 0))); + STAmount(d.share, -Number(875 - 21, 0))); } { @@ -3651,7 +3650,7 @@ class Vault_test : public beast::unit_test::suite STAmount(d.asset, Number(100, 0))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == - STAmount(d.share, Number(1000, 0))); + STAmount(d.share, Number(-1000, 0))); { testcase("Scale withdraw exact"); @@ -3679,7 +3678,7 @@ class Vault_test : public beast::unit_test::suite STAmount(d.asset, Number(90, 0))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == - STAmount(d.share, Number(900, 0))); + STAmount(d.share, Number(-900, 0))); } { @@ -3726,7 +3725,7 @@ class Vault_test : public beast::unit_test::suite STAmount(d.asset, Number(900 - 25, -1))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == - STAmount(d.share, Number(900 - 25, 0))); + STAmount(d.share, -Number(900 - 25, 0))); } { @@ -3755,7 +3754,7 @@ class Vault_test : public beast::unit_test::suite STAmount(d.asset, Number(875 - 38, -1))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == - STAmount(d.share, Number(875 - 38, 0))); + STAmount(d.share, -Number(875 - 38, 0))); } { @@ -3784,7 +3783,7 @@ class Vault_test : public beast::unit_test::suite STAmount(d.asset, Number(837 - 37, -1))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == - STAmount(d.share, Number(837 - 37, 0))); + STAmount(d.share, -Number(837 - 37, 0))); } { @@ -3807,7 +3806,7 @@ class Vault_test : public beast::unit_test::suite STAmount(d.asset, Number(800 - 1, -1))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == - STAmount(d.share, Number(800 - 1, 0))); + STAmount(d.share, -Number(800 - 1, 0))); } { @@ -3870,7 +3869,7 @@ class Vault_test : public beast::unit_test::suite STAmount(d.asset, Number(100, 0))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == - STAmount(d.share, Number(1000, 0))); + STAmount(d.share, -Number(1000, 0))); { testcase("Scale clawback exact"); // assetsToSharesWithdraw: @@ -3898,7 +3897,7 @@ class Vault_test : public beast::unit_test::suite STAmount(d.asset, Number(90, 0))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == - STAmount(d.share, Number(900, 0))); + STAmount(d.share, -Number(900, 0))); } { @@ -3938,7 +3937,7 @@ class Vault_test : public beast::unit_test::suite STAmount(d.asset, Number(900 - 25, -1))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == - STAmount(d.share, Number(900 - 25, 0))); + STAmount(d.share, -Number(900 - 25, 0))); } { @@ -3968,7 +3967,7 @@ class Vault_test : public beast::unit_test::suite STAmount(d.asset, Number(875 - 38, -1))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == - STAmount(d.share, Number(875 - 38, 0))); + STAmount(d.share, -Number(875 - 38, 0))); } { @@ -3998,7 +3997,7 @@ class Vault_test : public beast::unit_test::suite STAmount(d.asset, Number(837 - 37, -1))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == - STAmount(d.share, Number(837 - 37, 0))); + STAmount(d.share, -Number(837 - 37, 0))); } { @@ -4022,7 +4021,7 @@ class Vault_test : public beast::unit_test::suite STAmount(d.asset, Number(800 - 1, -1))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == - STAmount(d.share, Number(800 - 1, 0))); + STAmount(d.share, -Number(800 - 1, 0))); } { diff --git a/src/test/basics/FileUtilities_test.cpp b/src/test/basics/FileUtilities_test.cpp index 9071ac7231..d1a1d50216 100644 --- a/src/test/basics/FileUtilities_test.cpp +++ b/src/test/basics/FileUtilities_test.cpp @@ -31,7 +31,7 @@ public: void testGetFileContents() { - using namespace ripple::test::detail; + using namespace ripple::detail; using namespace boost::system; constexpr char const* expectedContents = diff --git a/src/test/basics/FeeUnits_test.cpp b/src/test/basics/Units_test.cpp similarity index 92% rename from src/test/basics/FeeUnits_test.cpp rename to src/test/basics/Units_test.cpp index f9be632644..33dce42abb 100644 --- a/src/test/basics/FeeUnits_test.cpp +++ b/src/test/basics/Units_test.cpp @@ -17,13 +17,13 @@ */ #include -#include #include +#include namespace ripple { namespace test { -class feeunits_test : public beast::unit_test::suite +class units_test : public beast::unit_test::suite { private: void @@ -35,16 +35,16 @@ private: XRPAmount x{100}; BEAST_EXPECT(x.drops() == 100); BEAST_EXPECT( - (std::is_same_v)); + (std::is_same_v)); auto y = 4u * x; BEAST_EXPECT(y.value() == 400); BEAST_EXPECT( - (std::is_same_v)); + (std::is_same_v)); auto z = 4 * y; BEAST_EXPECT(z.value() == 1600); BEAST_EXPECT( - (std::is_same_v)); + (std::is_same_v)); FeeLevel32 f{10}; FeeLevel32 baseFee{100}; @@ -55,7 +55,7 @@ private: BEAST_EXPECT(drops.value() == 1000); BEAST_EXPECT((std::is_same_v< std::remove_reference_t::unit_type, - feeunit::dropTag>)); + unit::dropTag>)); BEAST_EXPECT((std::is_same_v< std::remove_reference_t, @@ -65,11 +65,11 @@ private: XRPAmount x{100}; BEAST_EXPECT(x.value() == 100); BEAST_EXPECT( - (std::is_same_v)); + (std::is_same_v)); auto y = 4u * x; BEAST_EXPECT(y.value() == 400); BEAST_EXPECT( - (std::is_same_v)); + (std::is_same_v)); FeeLevel64 f{10}; FeeLevel64 baseFee{100}; @@ -80,7 +80,7 @@ private: BEAST_EXPECT(drops.value() == 1000); BEAST_EXPECT((std::is_same_v< std::remove_reference_t::unit_type, - feeunit::dropTag>)); + unit::dropTag>)); BEAST_EXPECT((std::is_same_v< std::remove_reference_t, XRPAmount>)); @@ -89,12 +89,12 @@ private: FeeLevel64 x{1024}; BEAST_EXPECT(x.value() == 1024); BEAST_EXPECT( - (std::is_same_v)); + (std::is_same_v)); std::uint64_t m = 4; auto y = m * x; BEAST_EXPECT(y.value() == 4096); BEAST_EXPECT( - (std::is_same_v)); + (std::is_same_v)); XRPAmount basefee{10}; FeeLevel64 referencefee{256}; @@ -105,7 +105,7 @@ private: BEAST_EXPECT(drops.value() == 40); BEAST_EXPECT((std::is_same_v< std::remove_reference_t::unit_type, - feeunit::dropTag>)); + unit::dropTag>)); BEAST_EXPECT((std::is_same_v< std::remove_reference_t, XRPAmount>)); @@ -181,7 +181,7 @@ private: void testFunctions() { - // Explicitly test every defined function for the TaggedFee class + // Explicitly test every defined function for the ValueUnit class // since some of them are templated, but not used anywhere else. using FeeLevel32 = FeeLevel; @@ -191,8 +191,8 @@ private: return FeeLevel64{x}; }; + [[maybe_unused]] FeeLevel64 defaulted; - (void)defaulted; FeeLevel64 test{0}; BEAST_EXPECT(test.fee() == 0); @@ -278,8 +278,8 @@ private: return FeeLevelDouble{x}; }; + [[maybe_unused]] FeeLevelDouble defaulted; - (void)defaulted; FeeLevelDouble test{0}; BEAST_EXPECT(test.fee() == 0); @@ -371,7 +371,7 @@ public: } }; -BEAST_DEFINE_TESTSUITE(feeunits, basics, ripple); +BEAST_DEFINE_TESTSUITE(units, basics, ripple); } // namespace test } // namespace ripple diff --git a/src/test/basics/base_uint_test.cpp b/src/test/basics/base_uint_test.cpp index 6ee9f0901a..458032d203 100644 --- a/src/test/basics/base_uint_test.cpp +++ b/src/test/basics/base_uint_test.cpp @@ -152,6 +152,7 @@ struct base_uint_test : beast::unit_test::suite uset.insert(u); BEAST_EXPECT(raw.size() == u.size()); BEAST_EXPECT(to_string(u) == "0102030405060708090A0B0C"); + BEAST_EXPECT(to_short_string(u) == "01020304..."); BEAST_EXPECT(*u.data() == 1); BEAST_EXPECT(u.signum() == 1); BEAST_EXPECT(!!u); @@ -174,6 +175,7 @@ struct base_uint_test : beast::unit_test::suite test96 v{~u}; uset.insert(v); BEAST_EXPECT(to_string(v) == "FEFDFCFBFAF9F8F7F6F5F4F3"); + BEAST_EXPECT(to_short_string(v) == "FEFDFCFB..."); BEAST_EXPECT(*v.data() == 0xfe); BEAST_EXPECT(v.signum() == 1); BEAST_EXPECT(!!v); @@ -194,6 +196,7 @@ struct base_uint_test : beast::unit_test::suite test96 z{beast::zero}; uset.insert(z); BEAST_EXPECT(to_string(z) == "000000000000000000000000"); + BEAST_EXPECT(to_short_string(z) == "00000000..."); BEAST_EXPECT(*z.data() == 0); BEAST_EXPECT(*z.begin() == 0); BEAST_EXPECT(*std::prev(z.end(), 1) == 0); @@ -214,6 +217,7 @@ struct base_uint_test : beast::unit_test::suite BEAST_EXPECT(n == z); n--; BEAST_EXPECT(to_string(n) == "FFFFFFFFFFFFFFFFFFFFFFFF"); + BEAST_EXPECT(to_short_string(n) == "FFFFFFFF..."); n = beast::zero; BEAST_EXPECT(n == z); @@ -224,6 +228,7 @@ struct base_uint_test : beast::unit_test::suite test96 x{zm1 ^ zp1}; uset.insert(x); BEAST_EXPECTS(to_string(x) == "FFFFFFFFFFFFFFFFFFFFFFFE", to_string(x)); + BEAST_EXPECTS(to_short_string(x) == "FFFFFFFF...", to_short_string(x)); BEAST_EXPECT(uset.size() == 4); diff --git a/src/test/consensus/NegativeUNL_test.cpp b/src/test/consensus/NegativeUNL_test.cpp index b56b834726..cc38ea5ab6 100644 --- a/src/test/consensus/NegativeUNL_test.cpp +++ b/src/test/consensus/NegativeUNL_test.cpp @@ -24,9 +24,9 @@ #include #include #include -#include #include +#include namespace ripple { namespace test { diff --git a/src/test/core/Config_test.cpp b/src/test/core/Config_test.cpp index 8b0fce1e20..a1a6a079cc 100644 --- a/src/test/core/Config_test.cpp +++ b/src/test/core/Config_test.cpp @@ -128,7 +128,7 @@ backend=sqlite /** Write a rippled config file and remove when done. */ -class RippledCfgGuard : public ripple::test::detail::FileDirGuard +class RippledCfgGuard : public ripple::detail::FileDirGuard { private: path dataDir_; @@ -239,7 +239,7 @@ moreripplevalidators.net /** Write a validators.txt file and remove when done. */ -class ValidatorsTxtGuard : public test::detail::FileDirGuard +class ValidatorsTxtGuard : public detail::FileDirGuard { public: ValidatorsTxtGuard( @@ -345,7 +345,7 @@ port_wss_admin { // read from file absolute path auto const cwd = current_path(); - ripple::test::detail::DirGuard const g0(*this, "test_db"); + ripple::detail::DirGuard const g0(*this, "test_db"); path const dataDirRel("test_data_dir"); path const dataDirAbs(cwd / g0.subdir() / dataDirRel); detail::RippledCfgGuard const g( diff --git a/src/test/csf/Digraph.h b/src/test/csf/Digraph.h index 3f079eac17..e65a7af913 100644 --- a/src/test/csf/Digraph.h +++ b/src/test/csf/Digraph.h @@ -30,9 +30,6 @@ #include namespace ripple { -namespace test { -namespace csf { - namespace detail { // Dummy class when no edge data needed for graph struct NoEdgeData @@ -41,6 +38,9 @@ struct NoEdgeData } // namespace detail +namespace test { +namespace csf { + /** Directed graph Basic directed graph that uses an adjacency list to represent out edges. diff --git a/src/test/jtx/Env.h b/src/test/jtx/Env.h index 21a239e3d7..68d8d3e53f 100644 --- a/src/test/jtx/Env.h +++ b/src/test/jtx/Env.h @@ -469,6 +469,9 @@ public: Returns 0 if the trust line does not exist. */ // VFALCO NOTE This should return a unit-less amount + PrettyAmount + balance(Account const& account, Asset const& asset) const; + PrettyAmount balance(Account const& account, Issue const& issue) const; diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index d4a39b6498..7d14f23c92 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -27,7 +27,9 @@ #include #include #include +#include #include +#include #include #include @@ -44,6 +46,252 @@ namespace ripple { namespace test { namespace jtx { +/** Generic helper class for helper clases that set a field on a JTx. + + Not every helper will be able to use this because of conversions and other + issues, but for classes where it's straightforward, this can simplify things. +*/ +template < + class SField, + class StoredValue = typename SField::type::value_type, + class OutputValue = StoredValue> +struct JTxField +{ + using SF = SField; + using SV = StoredValue; + using OV = OutputValue; + +protected: + SF const& sfield_; + SV value_; + +public: + explicit JTxField(SF const& sfield, SV const& value) + : sfield_(sfield), value_(value) + { + } + + virtual ~JTxField() = default; + + virtual OV + value() const = 0; + + virtual void + operator()(Env&, JTx& jt) const + { + jt.jv[sfield_.jsonName] = value(); + } +}; + +template +struct JTxField +{ + using SF = SField; + using SV = StoredValue; + using OV = SV; + +protected: + SF const& sfield_; + SV value_; + +public: + explicit JTxField(SF const& sfield, SV const& value) + : sfield_(sfield), value_(value) + { + } + + void + operator()(Env&, JTx& jt) const + { + jt.jv[sfield_.jsonName] = value_; + } +}; + +struct timePointField + : public JTxField +{ + using SF = SF_UINT32; + using SV = NetClock::time_point; + using OV = NetClock::rep; + using base = JTxField; + +protected: + using base::value_; + +public: + explicit timePointField(SF const& sfield, SV const& value) + : JTxField(sfield, value) + { + } + + OV + value() const override + { + return value_.time_since_epoch().count(); + } +}; + +struct uint256Field : public JTxField +{ + using SF = SF_UINT256; + using SV = uint256; + using OV = std::string; + using base = JTxField; + +protected: + using base::value_; + +public: + explicit uint256Field(SF const& sfield, SV const& value) + : JTxField(sfield, value) + { + } + + OV + value() const override + { + return to_string(value_); + } +}; + +struct accountIDField : public JTxField +{ + using SF = SF_ACCOUNT; + using SV = AccountID; + using OV = std::string; + using base = JTxField; + +protected: + using base::value_; + +public: + explicit accountIDField(SF const& sfield, SV const& value) + : JTxField(sfield, value) + { + } + + OV + value() const override + { + return toBase58(value_); + } +}; + +struct blobField : public JTxField +{ + using SF = SF_VL; + using SV = std::string; + using base = JTxField; + + using JTxField::JTxField; + + explicit blobField(SF const& sfield, Slice const& cond) + : JTxField(sfield, strHex(cond)) + { + } + + template + explicit blobField(SF const& sfield, std::array const& c) + : blobField(sfield, makeSlice(c)) + { + } +}; + +template +struct valueUnitField + : public JTxField, ValueType> +{ + using SF = SField; + using SV = unit::ValueUnit; + using OV = ValueType; + using base = JTxField; + + static_assert(std::is_same_v); + +protected: + using base::value_; + +public: + using JTxField::JTxField; + + OV + value() const override + { + return value_.value(); + } +}; + +template +struct JTxFieldWrapper +{ + using JF = JTxField; + using SF = typename JF::SF; + using SV = typename JF::SV; + +protected: + SF const& sfield_; + +public: + explicit JTxFieldWrapper(SF const& sfield) : sfield_(sfield) + { + } + + JF + operator()(SV const& value) const + { + return JTxField(sfield_, value); + } +}; + +template <> +struct JTxFieldWrapper +{ + using JF = blobField; + using SF = JF::SF; + using SV = JF::SV; + +protected: + SF const& sfield_; + +public: + explicit JTxFieldWrapper(SF const& sfield) : sfield_(sfield) + { + } + + JF + operator()(SV const& cond) const + { + return JF(sfield_, makeSlice(cond)); + } + + JF + operator()(Slice const& cond) const + { + return JF(sfield_, cond); + } + + template + JF + operator()(std::array const& c) const + { + return operator()(makeSlice(c)); + } +}; + +template < + class SField, + class UnitTag, + class ValueType = typename SField::type::value_type> +using valueUnitWrapper = + JTxFieldWrapper>; + +template +using simpleField = JTxFieldWrapper>; + +/** General field definitions, or fields used in multiple transaction namespaces + */ +auto const data = JTxFieldWrapper(sfData); + // TODO We only need this long "requires" clause as polyfill, for C++20 // implementations which are missing header. Replace with // `std::ranges::range`, and accordingly use std::ranges::begin/end @@ -111,6 +359,25 @@ checkArraySize(Json::Value const& val, unsigned int size); std::uint32_t ownerCount(test::jtx::Env const& env, test::jtx::Account const& account); +[[nodiscard]] +inline bool +checkVL(Slice const& result, std::string const& expected) +{ + Serializer s; + s.addRaw(result); + return s.getString() == expected; +} + +[[nodiscard]] +inline bool +checkVL( + std::shared_ptr const& sle, + SField const& field, + std::string const& expected) +{ + return strHex(expected) == strHex(sle->getFieldVL(field)); +} + /* Path finding */ /******************************************************************************/ void @@ -186,7 +453,7 @@ PrettyAmount xrpMinusFee(Env const& env, std::int64_t xrpAmount); bool -expectLine( +expectHolding( Env& env, AccountID const& account, STAmount const& value, @@ -194,18 +461,18 @@ expectLine( template bool -expectLine( +expectHolding( Env& env, AccountID const& account, STAmount const& value, Amts const&... amts) { - return expectLine(env, account, value, false) && - expectLine(env, account, amts...); + return expectHolding(env, account, value, false) && + expectHolding(env, account, amts...); } bool -expectLine(Env& env, AccountID const& account, None const& value); +expectHolding(Env& env, AccountID const& account, None const& value); bool expectOffers( diff --git a/src/test/jtx/amount.h b/src/test/jtx/amount.h index 344a2ab73c..a793f3a287 100644 --- a/src/test/jtx/amount.h +++ b/src/test/jtx/amount.h @@ -24,9 +24,9 @@ #include #include -#include #include #include +#include #include #include @@ -34,6 +34,15 @@ #include namespace ripple { +namespace detail { + +struct epsilon_multiple +{ + std::size_t n; +}; + +} // namespace detail + namespace test { namespace jtx { @@ -57,7 +66,7 @@ struct AnyAmount; // struct None { - Issue issue; + Asset asset; }; //------------------------------------------------------------------------------ @@ -133,6 +142,12 @@ public: return amount_; } + inline int + signum() const + { + return amount_.signum(); + } + operator STAmount const&() const { return amount_; @@ -165,17 +180,17 @@ struct PrettyAsset { private: Asset asset_; - unsigned int scale_; + std::uint32_t scale_; public: template requires std::convertible_to - PrettyAsset(A const& asset, unsigned int scale = 1) + PrettyAsset(A const& asset, std::uint32_t scale = 1) : PrettyAsset{Asset{asset}, scale} { } - PrettyAsset(Asset const& asset, unsigned int scale = 1) + PrettyAsset(Asset const& asset, std::uint32_t scale = 1) : asset_(asset), scale_(scale) { } @@ -199,10 +214,22 @@ public: template PrettyAmount operator()(T v) const + { + return operator()(Number(v)); + } + + PrettyAmount + operator()(Number v) const { STAmount amount{asset_, v * scale_}; return {amount, ""}; } + + None + operator()(none_t) const + { + return {asset_}; + } }; //------------------------------------------------------------------------------ @@ -312,15 +339,6 @@ drops(XRPAmount i) //------------------------------------------------------------------------------ -namespace detail { - -struct epsilon_multiple -{ - std::size_t n; -}; - -} // namespace detail - // The smallest possible IOU STAmount struct epsilon_t { @@ -360,6 +378,11 @@ public: { return {currency, account.id()}; } + Asset + asset() const + { + return issue(); + } /** Implicit conversion to Issue or Asset. @@ -370,9 +393,9 @@ public: { return issue(); } - operator Asset() const + operator PrettyAsset() const { - return issue(); + return asset(); } template < @@ -438,14 +461,32 @@ public: return issuanceID; } - /** Implicit conversion to MPTIssue. + /** Explicit conversion to MPTIssue or asset. + */ + ripple::MPTIssue + mptIssue() const + { + return MPTIssue{issuanceID}; + } + Asset + asset() const + { + return mptIssue(); + } + + /** Implicit conversion to MPTIssue or asset. This allows passing an MPT value where an MPTIssue is expected. */ operator ripple::MPTIssue() const { - return MPTIssue{issuanceID}; + return mptIssue(); + } + + operator PrettyAsset() const + { + return asset(); } template @@ -461,6 +502,13 @@ public: PrettyAmount operator()(detail::epsilon_multiple) const; + /** Returns None-of-Issue */ + None + operator()(none_t) const + { + return {mptIssue()}; + } + friend BookSpec operator~(MPT const& mpt) { diff --git a/src/test/jtx/balance.h b/src/test/jtx/balance.h index 3a2cf0423f..0c4a6cca1d 100644 --- a/src/test/jtx/balance.h +++ b/src/test/jtx/balance.h @@ -38,9 +38,9 @@ namespace jtx { class balance { private: - bool none_; - Account account_; - STAmount value_; + bool const none_; + Account const account_; + STAmount const value_; public: balance(Account const& account, none_t) @@ -49,7 +49,7 @@ public: } balance(Account const& account, None const& value) - : none_(true), account_(account), value_(value.issue) + : none_(true), account_(account), value_(value.asset) { } diff --git a/src/test/jtx/escrow.h b/src/test/jtx/escrow.h index 3147b44c65..483db578b0 100644 --- a/src/test/jtx/escrow.h +++ b/src/test/jtx/escrow.h @@ -22,6 +22,7 @@ #include #include +#include #include #include @@ -94,86 +95,14 @@ std::array const cb3 = { 0x57, 0x0D, 0x15, 0x85, 0x8B, 0xD4, 0x81, 0x01, 0x04}}; /** Set the "FinishAfter" time tag on a JTx */ -struct finish_time -{ -private: - NetClock::time_point value_; - -public: - explicit finish_time(NetClock::time_point const& value) : value_(value) - { - } - - void - operator()(Env&, JTx& jt) const - { - jt.jv[sfFinishAfter.jsonName] = value_.time_since_epoch().count(); - } -}; +auto const finish_time = JTxFieldWrapper(sfFinishAfter); /** Set the "CancelAfter" time tag on a JTx */ -struct cancel_time -{ -private: - NetClock::time_point value_; +auto const cancel_time = JTxFieldWrapper(sfCancelAfter); -public: - explicit cancel_time(NetClock::time_point const& value) : value_(value) - { - } +auto const condition = JTxFieldWrapper(sfCondition); - void - operator()(jtx::Env&, jtx::JTx& jt) const - { - jt.jv[sfCancelAfter.jsonName] = value_.time_since_epoch().count(); - } -}; - -struct condition -{ -private: - std::string value_; - -public: - explicit condition(Slice const& cond) : value_(strHex(cond)) - { - } - - template - explicit condition(std::array const& c) - : condition(makeSlice(c)) - { - } - - void - operator()(Env&, JTx& jt) const - { - jt.jv[sfCondition.jsonName] = value_; - } -}; - -struct fulfillment -{ -private: - std::string value_; - -public: - explicit fulfillment(Slice condition) : value_(strHex(condition)) - { - } - - template - explicit fulfillment(std::array f) - : fulfillment(makeSlice(f)) - { - } - - void - operator()(Env&, JTx& jt) const - { - jt.jv[sfFulfillment.jsonName] = value_; - } -}; +auto const fulfillment = JTxFieldWrapper(sfFulfillment); } // namespace escrow diff --git a/src/test/jtx/fee.h b/src/test/jtx/fee.h index 7d54804f87..3e3740b80d 100644 --- a/src/test/jtx/fee.h +++ b/src/test/jtx/fee.h @@ -37,6 +37,7 @@ class fee { private: bool manual_ = true; + bool increment_ = false; std::optional amount_; public: @@ -44,6 +45,10 @@ public: { } + explicit fee(increment_t) : increment_(true) + { + } + explicit fee(none_t) { } diff --git a/src/test/jtx/flags.h b/src/test/jtx/flags.h index aa048c3e55..8d3fa4f25c 100644 --- a/src/test/jtx/flags.h +++ b/src/test/jtx/flags.h @@ -27,22 +27,6 @@ #include namespace ripple { -namespace test { -namespace jtx { - -// JSON generators - -/** Add and/or remove flag. */ -Json::Value -fset(Account const& account, std::uint32_t on, std::uint32_t off = 0); - -/** Remove account flag. */ -inline Json::Value -fclear(Account const& account, std::uint32_t off) -{ - return fset(account, 0, off); -} - namespace detail { class flags_helper @@ -123,6 +107,22 @@ protected: } // namespace detail +namespace test { +namespace jtx { + +// JSON generators + +/** Add and/or remove flag. */ +Json::Value +fset(Account const& account, std::uint32_t on, std::uint32_t off = 0); + +/** Remove account flag. */ +inline Json::Value +fclear(Account const& account, std::uint32_t off) +{ + return fset(account, 0, off); +} + /** Match set account flags */ class flags : private detail::flags_helper { diff --git a/src/test/jtx/impl/Env.cpp b/src/test/jtx/impl/Env.cpp index d6956b30c7..ae99e1b5d6 100644 --- a/src/test/jtx/impl/Env.cpp +++ b/src/test/jtx/impl/Env.cpp @@ -218,7 +218,9 @@ Env::balance(Account const& account, MPTIssue const& mptIssue) const if (!sle) return {STAmount(mptIssue, 0), account.name()}; - STAmount const amount{mptIssue, sle->getFieldU64(sfOutstandingAmount)}; + // Make it negative + STAmount const amount{ + mptIssue, sle->getFieldU64(sfOutstandingAmount), 0, true}; return {amount, lookup(issuer).name()}; } else @@ -233,6 +235,14 @@ Env::balance(Account const& account, MPTIssue const& mptIssue) const } } +PrettyAmount +Env::balance(Account const& account, Asset const& asset) const +{ + return std::visit( + [&](auto const& issue) { return balance(account, issue); }, + asset.value()); +} + PrettyAmount Env::limit(Account const& account, Issue const& issue) const { diff --git a/src/test/jtx/impl/Oracle.cpp b/src/test/jtx/impl/Oracle.cpp index 721a1c299d..97a31cbb0c 100644 --- a/src/test/jtx/impl/Oracle.cpp +++ b/src/test/jtx/impl/Oracle.cpp @@ -317,10 +317,10 @@ Oracle::ledgerEntry( if (jr.isObject()) { + if (jr.isMember(jss::error)) + return jr; if (jr.isMember(jss::result) && jr[jss::result].isMember(jss::status)) return jr[jss::result]; - else if (jr.isMember(jss::error)) - return jr; } return Json::nullValue; } diff --git a/src/test/jtx/impl/TestHelpers.cpp b/src/test/jtx/impl/TestHelpers.cpp index 5f8c53877a..20f24f0d84 100644 --- a/src/test/jtx/impl/TestHelpers.cpp +++ b/src/test/jtx/impl/TestHelpers.cpp @@ -103,7 +103,7 @@ xrpMinusFee(Env const& env, std::int64_t xrpAmount) }; [[nodiscard]] bool -expectLine( +expectHolding( Env& env, AccountID const& account, STAmount const& value, @@ -137,9 +137,33 @@ expectLine( } [[nodiscard]] bool -expectLine(Env& env, AccountID const& account, None const& value) +expectHolding( + Env& env, + AccountID const& account, + None const&, + Issue const& issue) { - return !env.le(keylet::line(account, value.issue)); + return !env.le(keylet::line(account, issue)); +} + +[[nodiscard]] bool +expectHolding( + Env& env, + AccountID const& account, + None const&, + MPTIssue const& mptIssue) +{ + return !env.le(keylet::mptoken(mptIssue.getMptID(), account)); +} + +[[nodiscard]] bool +expectHolding(Env& env, AccountID const& account, None const& value) +{ + return std::visit( + [&](auto const& issue) { + return expectHolding(env, account, value, issue); + }, + value.asset.value()); } [[nodiscard]] bool diff --git a/src/test/jtx/impl/amount.cpp b/src/test/jtx/impl/amount.cpp index a1dbd25652..a04a6c87d2 100644 --- a/src/test/jtx/impl/amount.cpp +++ b/src/test/jtx/impl/amount.cpp @@ -91,12 +91,18 @@ operator<<(std::ostream& os, PrettyAmount const& amount) os << to_places(d, 6) << " XRP"; } - else + else if (amount.value().holds()) { os << amount.value().getText() << "/" << to_string(amount.value().issue().currency) << "(" << amount.name() << ")"; } + else + { + auto const& mptIssue = amount.value().asset().get(); + os << amount.value().getText() << "/" << to_string(mptIssue) << "(" + << amount.name() << ")"; + } return os; } diff --git a/src/test/jtx/impl/balance.cpp b/src/test/jtx/impl/balance.cpp index 42330658eb..decbd816e1 100644 --- a/src/test/jtx/impl/balance.cpp +++ b/src/test/jtx/impl/balance.cpp @@ -24,38 +24,73 @@ namespace test { namespace jtx { void -balance::operator()(Env& env) const +doBalance( + Env& env, + AccountID const& account, + bool none, + STAmount const& value, + Issue const& issue) { - if (isXRP(value_.issue())) + if (isXRP(issue)) { - auto const sle = env.le(account_); - if (none_) + auto const sle = env.le(keylet::account(account)); + if (none) { env.test.expect(!sle); } else if (env.test.expect(sle)) { - env.test.expect(sle->getFieldAmount(sfBalance) == value_); + env.test.expect(sle->getFieldAmount(sfBalance) == value); } } else { - auto const sle = env.le(keylet::line(account_.id(), value_.issue())); - if (none_) + auto const sle = env.le(keylet::line(account, issue)); + if (none) { env.test.expect(!sle); } else if (env.test.expect(sle)) { auto amount = sle->getFieldAmount(sfBalance); - amount.setIssuer(value_.issue().account); - if (account_.id() > value_.issue().account) + amount.setIssuer(issue.account); + if (account > issue.account) amount.negate(); - env.test.expect(amount == value_); + env.test.expect(amount == value); } } } +void +doBalance( + Env& env, + AccountID const& account, + bool none, + STAmount const& value, + MPTIssue const& mptIssue) +{ + auto const sle = env.le(keylet::mptoken(mptIssue.getMptID(), account)); + if (none) + { + env.test.expect(!sle); + } + else if (env.test.expect(sle)) + { + STAmount const amount{mptIssue, sle->getFieldU64(sfMPTAmount)}; + env.test.expect(amount == value); + } +} + +void +balance::operator()(Env& env) const +{ + return std::visit( + [&](auto const& issue) { + doBalance(env, account_.id(), none_, value_, issue); + }, + value_.asset().value()); +} + } // namespace jtx } // namespace test } // namespace ripple diff --git a/src/test/jtx/impl/fee.cpp b/src/test/jtx/impl/fee.cpp index 71e3dd089a..a58105a85f 100644 --- a/src/test/jtx/impl/fee.cpp +++ b/src/test/jtx/impl/fee.cpp @@ -26,13 +26,16 @@ namespace test { namespace jtx { void -fee::operator()(Env&, JTx& jt) const +fee::operator()(Env& env, JTx& jt) const { if (!manual_) return; jt.fill_fee = false; - if (amount_) - jt[jss::Fee] = amount_->getJson(JsonOptions::none); + assert(!increment_ || !amount_); + if (increment_) + jt[sfFee] = STAmount(env.current()->fees().increment).getJson(); + else if (amount_) + jt[sfFee] = amount_->getJson(JsonOptions::none); } } // namespace jtx diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index 9f7a611feb..f2f51492e3 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -102,6 +102,8 @@ MPTTester::create(MPTCreate const& arg) jv[sfMaximumAmount] = std::to_string(*arg.maxAmt); if (arg.domainID) jv[sfDomainID] = to_string(*arg.domainID); + if (arg.mutableFlags) + jv[sfMutableFlags] = *arg.mutableFlags; if (submit(arg, jv) != tesSUCCESS) { // Verify issuance doesn't exist @@ -240,19 +242,59 @@ MPTTester::set(MPTSet const& arg) jv[sfDelegate] = arg.delegate->human(); if (arg.domainID) jv[sfDomainID] = to_string(*arg.domainID); - if (submit(arg, jv) == tesSUCCESS && arg.flags.value_or(0)) + if (arg.mutableFlags) + jv[sfMutableFlags] = *arg.mutableFlags; + if (arg.transferFee) + jv[sfTransferFee] = *arg.transferFee; + if (arg.metadata) + jv[sfMPTokenMetadata] = strHex(*arg.metadata); + if (submit(arg, jv) == tesSUCCESS && (arg.flags || arg.mutableFlags)) { auto require = [&](std::optional const& holder, bool unchanged) { auto flags = getFlags(holder); if (!unchanged) { - if (*arg.flags & tfMPTLock) - flags |= lsfMPTLocked; - else if (*arg.flags & tfMPTUnlock) - flags &= ~lsfMPTLocked; - else - Throw("Invalid flags"); + if (arg.flags) + { + if (*arg.flags & tfMPTLock) + flags |= lsfMPTLocked; + else if (*arg.flags & tfMPTUnlock) + flags &= ~lsfMPTLocked; + } + + if (arg.mutableFlags) + { + if (*arg.mutableFlags & tmfMPTSetCanLock) + flags |= lsfMPTCanLock; + else if (*arg.mutableFlags & tmfMPTClearCanLock) + flags &= ~lsfMPTCanLock; + + if (*arg.mutableFlags & tmfMPTSetRequireAuth) + flags |= lsfMPTRequireAuth; + else if (*arg.mutableFlags & tmfMPTClearRequireAuth) + flags &= ~lsfMPTRequireAuth; + + if (*arg.mutableFlags & tmfMPTSetCanEscrow) + flags |= lsfMPTCanEscrow; + else if (*arg.mutableFlags & tmfMPTClearCanEscrow) + flags &= ~lsfMPTCanEscrow; + + if (*arg.mutableFlags & tmfMPTSetCanClawback) + flags |= lsfMPTCanClawback; + else if (*arg.mutableFlags & tmfMPTClearCanClawback) + flags &= ~lsfMPTCanClawback; + + if (*arg.mutableFlags & tmfMPTSetCanTrade) + flags |= lsfMPTCanTrade; + else if (*arg.mutableFlags & tmfMPTClearCanTrade) + flags &= ~lsfMPTCanTrade; + + if (*arg.mutableFlags & tmfMPTSetCanTransfer) + flags |= lsfMPTCanTransfer; + else if (*arg.mutableFlags & tmfMPTClearCanTransfer) + flags &= ~lsfMPTCanTransfer; + } } env_.require(mptflags(*this, flags, holder)); }; @@ -313,6 +355,43 @@ MPTTester::checkFlags( return expectedFlags == getFlags(holder); } +[[nodiscard]] bool +MPTTester::checkMetadata(std::string const& metadata) const +{ + return forObject([&](SLEP const& sle) -> bool { + if (sle->isFieldPresent(sfMPTokenMetadata)) + return strHex(sle->getFieldVL(sfMPTokenMetadata)) == + strHex(metadata); + return false; + }); +} + +[[nodiscard]] bool +MPTTester::isMetadataPresent() const +{ + return forObject([&](SLEP const& sle) -> bool { + return sle->isFieldPresent(sfMPTokenMetadata); + }); +} + +[[nodiscard]] bool +MPTTester::checkTransferFee(std::uint16_t transferFee) const +{ + return forObject([&](SLEP const& sle) -> bool { + if (sle->isFieldPresent(sfTransferFee)) + return sle->getFieldU16(sfTransferFee) == transferFee; + return false; + }); +} + +[[nodiscard]] bool +MPTTester::isTransferFeePresent() const +{ + return forObject([&](SLEP const& sle) -> bool { + return sle->isFieldPresent(sfTransferFee); + }); +} + void MPTTester::pay( Account const& src, diff --git a/src/test/jtx/impl/owners.cpp b/src/test/jtx/impl/owners.cpp index 386ec29a37..b55986fccb 100644 --- a/src/test/jtx/impl/owners.cpp +++ b/src/test/jtx/impl/owners.cpp @@ -20,9 +20,6 @@ #include namespace ripple { -namespace test { -namespace jtx { - namespace detail { std::uint32_t @@ -39,7 +36,7 @@ owned_count_of(ReadView const& view, AccountID const& id, LedgerEntryType type) void owned_count_helper( - Env& env, + test::jtx::Env& env, AccountID const& id, LedgerEntryType type, std::uint32_t value) @@ -49,6 +46,9 @@ owned_count_helper( } // namespace detail +namespace test { +namespace jtx { + void owners::operator()(Env& env) const { diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h index 4756ca723d..2eacac68ec 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -106,6 +106,7 @@ struct MPTCreate std::optional holderCount = std::nullopt; bool fund = true; std::optional flags = {0}; + std::optional mutableFlags = std::nullopt; std::optional domainID = std::nullopt; std::optional err = std::nullopt; }; @@ -139,6 +140,9 @@ struct MPTSet std::optional ownerCount = std::nullopt; std::optional holderCount = std::nullopt; std::optional flags = std::nullopt; + std::optional mutableFlags = std::nullopt; + std::optional transferFee = std::nullopt; + std::optional metadata = std::nullopt; std::optional delegate = std::nullopt; std::optional domainID = std::nullopt; std::optional err = std::nullopt; @@ -182,6 +186,18 @@ public: uint32_t const expectedFlags, std::optional const& holder = std::nullopt) const; + [[nodiscard]] bool + checkMetadata(std::string const& metadata) const; + + [[nodiscard]] bool + isMetadataPresent() const; + + [[nodiscard]] bool + checkTransferFee(std::uint16_t transferFee) const; + + [[nodiscard]] bool + isTransferFeePresent() const; + Account const& issuer() const { diff --git a/src/test/jtx/owners.h b/src/test/jtx/owners.h index fc904f9e87..baf5d9da7d 100644 --- a/src/test/jtx/owners.h +++ b/src/test/jtx/owners.h @@ -22,16 +22,13 @@ #include -#include - +#include #include #include #include namespace ripple { -namespace test { -namespace jtx { namespace detail { @@ -40,13 +37,16 @@ owned_count_of(ReadView const& view, AccountID const& id, LedgerEntryType type); void owned_count_helper( - Env& env, + test::jtx::Env& env, AccountID const& id, LedgerEntryType type, std::uint32_t value); } // namespace detail +namespace test { +namespace jtx { + // Helper for aliases template class owner_count diff --git a/src/test/jtx/require.h b/src/test/jtx/require.h index bec21235a6..3215ac0abb 100644 --- a/src/test/jtx/require.h +++ b/src/test/jtx/require.h @@ -26,14 +26,12 @@ #include namespace ripple { -namespace test { -namespace jtx { namespace detail { template inline void -require_args(requires_t& vec, Cond const& cond, Args const&... args) +require_args(test::jtx::requires_t& vec, Cond const& cond, Args const&... args) { vec.push_back(cond); if constexpr (sizeof...(args) > 0) @@ -42,6 +40,9 @@ require_args(requires_t& vec, Cond const& cond, Args const&... args) } // namespace detail +namespace test { +namespace jtx { + /** Compose many condition functors into one */ template require_t diff --git a/src/test/jtx/tags.h b/src/test/jtx/tags.h index bb64295f05..4d55929d69 100644 --- a/src/test/jtx/tags.h +++ b/src/test/jtx/tags.h @@ -49,6 +49,16 @@ struct disabled_t }; static disabled_t const disabled; +/** Used for fee() calls that use an owner reserve increment */ +struct increment_t +{ + increment_t() + { + } +}; + +static increment_t const increment; + } // namespace jtx } // namespace test diff --git a/src/test/ledger/BookDirs_test.cpp b/src/test/ledger/BookDirs_test.cpp index 52b618e9a0..48ee92a6fd 100644 --- a/src/test/ledger/BookDirs_test.cpp +++ b/src/test/ledger/BookDirs_test.cpp @@ -17,8 +17,7 @@ #include -#include - +#include #include namespace ripple { diff --git a/src/test/ledger/Directory_test.cpp b/src/test/ledger/Directory_test.cpp index 9e8d40e0cc..fe8f04523f 100644 --- a/src/test/ledger/Directory_test.cpp +++ b/src/test/ledger/Directory_test.cpp @@ -17,10 +17,9 @@ #include -#include -#include - #include +#include +#include #include #include #include diff --git a/src/test/ledger/PaymentSandbox_test.cpp b/src/test/ledger/PaymentSandbox_test.cpp index 26b06a0034..db7fbed019 100644 --- a/src/test/ledger/PaymentSandbox_test.cpp +++ b/src/test/ledger/PaymentSandbox_test.cpp @@ -19,10 +19,9 @@ #include -#include -#include -#include - +#include +#include +#include #include #include diff --git a/src/test/ledger/SkipList_test.cpp b/src/test/ledger/SkipList_test.cpp index c2088d77cf..e0f4049c73 100644 --- a/src/test/ledger/SkipList_test.cpp +++ b/src/test/ledger/SkipList_test.cpp @@ -20,9 +20,9 @@ #include #include -#include #include +#include namespace ripple { namespace test { diff --git a/src/test/ledger/View_test.cpp b/src/test/ledger/View_test.cpp index 4af3e37ce2..17d3244aa2 100644 --- a/src/test/ledger/View_test.cpp +++ b/src/test/ledger/View_test.cpp @@ -21,11 +21,11 @@ #include #include -#include -#include -#include -#include +#include +#include +#include +#include #include #include diff --git a/src/test/nodestore/import_test.cpp b/src/test/nodestore/import_test.cpp index 11009ec5be..ea5f23548a 100644 --- a/src/test/nodestore/import_test.cpp +++ b/src/test/nodestore/import_test.cpp @@ -61,7 +61,6 @@ multi(32gb): */ namespace ripple { -namespace NodeStore { namespace detail { @@ -191,6 +190,8 @@ fmtdur(std::chrono::duration const& d) } // namespace detail +namespace NodeStore { + //------------------------------------------------------------------------------ class progress diff --git a/src/test/protocol/STAccount_test.cpp b/src/test/protocol/STAccount_test.cpp index 9476a47c5e..cc318b4458 100644 --- a/src/test/protocol/STAccount_test.cpp +++ b/src/test/protocol/STAccount_test.cpp @@ -122,10 +122,27 @@ struct STAccount_test : public beast::unit_test::suite } } + void + testAccountID() + { + auto const s = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + if (auto const parsed = parseBase58(s); BEAST_EXPECT(parsed)) + { + BEAST_EXPECT(toBase58(*parsed) == s); + } + + { + auto const s = + "âabcd1rNxp4h8apvRis6mJf9Sh8C6iRxfrDWNâabcdAVâ\xc2\x80\xc2\x8f"; + BEAST_EXPECT(!parseBase58(s)); + } + } + void run() override { testSTAccount(); + testAccountID(); } }; diff --git a/src/test/protocol/STInteger_test.cpp b/src/test/protocol/STInteger_test.cpp new file mode 100644 index 0000000000..6c4cdd6fcf --- /dev/null +++ b/src/test/protocol/STInteger_test.cpp @@ -0,0 +1,163 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include +#include + +namespace ripple { + +struct STInteger_test : public beast::unit_test::suite +{ + void + testUInt8() + { + testcase("UInt8"); + STUInt8 u8(255); + BEAST_EXPECT(u8.value() == 255); + BEAST_EXPECT(u8.getText() == "255"); + BEAST_EXPECT(u8.getSType() == STI_UINT8); + BEAST_EXPECT(u8.getJson(JsonOptions::none) == 255); + + // there is some special handling for sfTransactionResult + STUInt8 tr(sfTransactionResult, 0); + BEAST_EXPECT(tr.value() == 0); + BEAST_EXPECT( + tr.getText() == + "The transaction was applied. Only final in a validated ledger."); + BEAST_EXPECT(tr.getSType() == STI_UINT8); + BEAST_EXPECT(tr.getJson(JsonOptions::none) == "tesSUCCESS"); + + // invalid transaction result + STUInt8 tr2(sfTransactionResult, 255); + BEAST_EXPECT(tr2.value() == 255); + BEAST_EXPECT(tr2.getText() == "255"); + BEAST_EXPECT(tr2.getSType() == STI_UINT8); + BEAST_EXPECT(tr2.getJson(JsonOptions::none) == 255); + } + + void + testUInt16() + { + testcase("UInt16"); + STUInt16 u16(65535); + BEAST_EXPECT(u16.value() == 65535); + BEAST_EXPECT(u16.getText() == "65535"); + BEAST_EXPECT(u16.getSType() == STI_UINT16); + BEAST_EXPECT(u16.getJson(JsonOptions::none) == 65535); + + // there is some special handling for sfLedgerEntryType + STUInt16 let(sfLedgerEntryType, ltACCOUNT_ROOT); + BEAST_EXPECT(let.value() == ltACCOUNT_ROOT); + BEAST_EXPECT(let.getText() == "AccountRoot"); + BEAST_EXPECT(let.getSType() == STI_UINT16); + BEAST_EXPECT(let.getJson(JsonOptions::none) == "AccountRoot"); + + // there is some special handling for sfTransactionType + STUInt16 tlt(sfTransactionType, ttPAYMENT); + BEAST_EXPECT(tlt.value() == ttPAYMENT); + BEAST_EXPECT(tlt.getText() == "Payment"); + BEAST_EXPECT(tlt.getSType() == STI_UINT16); + BEAST_EXPECT(tlt.getJson(JsonOptions::none) == "Payment"); + } + + void + testUInt32() + { + testcase("UInt32"); + STUInt32 u32(4'294'967'295u); + BEAST_EXPECT(u32.value() == 4'294'967'295u); + BEAST_EXPECT(u32.getText() == "4294967295"); + BEAST_EXPECT(u32.getSType() == STI_UINT32); + BEAST_EXPECT(u32.getJson(JsonOptions::none) == 4'294'967'295u); + + // there is some special handling for sfPermissionValue + STUInt32 pv(sfPermissionValue, ttPAYMENT + 1); + BEAST_EXPECT(pv.value() == ttPAYMENT + 1); + BEAST_EXPECT(pv.getText() == "Payment"); + BEAST_EXPECT(pv.getSType() == STI_UINT32); + BEAST_EXPECT(pv.getJson(JsonOptions::none) == "Payment"); + STUInt32 pv2(sfPermissionValue, PaymentMint); + BEAST_EXPECT(pv2.value() == PaymentMint); + BEAST_EXPECT(pv2.getText() == "PaymentMint"); + BEAST_EXPECT(pv2.getSType() == STI_UINT32); + BEAST_EXPECT(pv2.getJson(JsonOptions::none) == "PaymentMint"); + } + + void + testUInt64() + { + testcase("UInt64"); + STUInt64 u64(0xFFFFFFFFFFFFFFFFull); + BEAST_EXPECT(u64.value() == 0xFFFFFFFFFFFFFFFFull); + BEAST_EXPECT(u64.getText() == "18446744073709551615"); + BEAST_EXPECT(u64.getSType() == STI_UINT64); + + // By default, getJson returns hex string + auto jsonVal = u64.getJson(JsonOptions::none); + BEAST_EXPECT(jsonVal.isString()); + BEAST_EXPECT(jsonVal.asString() == "ffffffffffffffff"); + + STUInt64 u64_2(sfMaximumAmount, 0xFFFFFFFFFFFFFFFFull); + BEAST_EXPECT(u64_2.value() == 0xFFFFFFFFFFFFFFFFull); + BEAST_EXPECT(u64_2.getText() == "18446744073709551615"); + BEAST_EXPECT(u64_2.getSType() == STI_UINT64); + BEAST_EXPECT( + u64_2.getJson(JsonOptions::none) == "18446744073709551615"); + } + + void + testInt32() + { + testcase("Int32"); + { + int const minInt32 = -2147483648; + STInt32 i32(minInt32); + BEAST_EXPECT(i32.value() == minInt32); + BEAST_EXPECT(i32.getText() == "-2147483648"); + BEAST_EXPECT(i32.getSType() == STI_INT32); + BEAST_EXPECT(i32.getJson(JsonOptions::none) == minInt32); + } + + { + int const maxInt32 = 2147483647; + STInt32 i32(maxInt32); + BEAST_EXPECT(i32.value() == maxInt32); + BEAST_EXPECT(i32.getText() == "2147483647"); + BEAST_EXPECT(i32.getSType() == STI_INT32); + BEAST_EXPECT(i32.getJson(JsonOptions::none) == maxInt32); + } + } + + void + run() override + { + testUInt8(); + testUInt16(); + testUInt32(); + testUInt64(); + testInt32(); + } +}; + +BEAST_DEFINE_TESTSUITE(STInteger, protocol, ripple); + +} // namespace ripple diff --git a/src/test/protocol/STObject_test.cpp b/src/test/protocol/STObject_test.cpp index e02cfeeea4..47a7ab0ad2 100644 --- a/src/test/protocol/STObject_test.cpp +++ b/src/test/protocol/STObject_test.cpp @@ -19,223 +19,11 @@ #include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - namespace ripple { class STObject_test : public beast::unit_test::suite { public: - bool - parseJSONString(std::string const& json, Json::Value& to) - { - Json::Reader reader; - return reader.parse(json, to) && to.isObject(); - } - - void - testParseJSONArrayWithInvalidChildrenObjects() - { - testcase("parse json array invalid children"); - try - { - /* - - STArray/STObject constructs don't really map perfectly to json - arrays/objects. - - STObject is an associative container, mapping fields to value, but - an STObject may also have a Field as its name, stored outside the - associative structure. The name is important, so to maintain - fidelity, it will take TWO json objects to represent them. - - */ - std::string faulty( - "{\"Template\":[{" - "\"ModifiedNode\":{\"Sequence\":1}, " - "\"DeletedNode\":{\"Sequence\":1}" - "}]}"); - - std::unique_ptr so; - Json::Value faultyJson; - bool parsedOK(parseJSONString(faulty, faultyJson)); - unexpected(!parsedOK, "failed to parse"); - STParsedJSONObject parsed("test", faultyJson); - BEAST_EXPECT(!parsed.object); - } - catch (std::runtime_error& e) - { - std::string what(e.what()); - unexpected(what.find("First level children of `Template`") != 0); - } - } - - void - testParseJSONArray() - { - testcase("parse json array"); - std::string const json( - "{\"Template\":[{\"ModifiedNode\":{\"Sequence\":1}}]}"); - - Json::Value jsonObject; - bool parsedOK(parseJSONString(json, jsonObject)); - if (parsedOK) - { - STParsedJSONObject parsed("test", jsonObject); - BEAST_EXPECT(parsed.object); - std::string const& serialized( - to_string(parsed.object->getJson(JsonOptions::none))); - BEAST_EXPECT(serialized == json); - } - else - { - fail("Couldn't parse json: " + json); - } - } - - void - testParseJSONEdgeCases() - { - testcase("parse json object"); - - { - std::string const goodJson(R"({"CloseResolution":19,"Method":250,)" - R"("TransactionResult":"tecFROZEN"})"); - - Json::Value jv; - if (BEAST_EXPECT(parseJSONString(goodJson, jv))) - { - STParsedJSONObject parsed("test", jv); - if (BEAST_EXPECT(parsed.object)) - { - std::string const& serialized( - to_string(parsed.object->getJson(JsonOptions::none))); - BEAST_EXPECT(serialized == goodJson); - } - } - } - - { - std::string const goodJson( - R"({"CloseResolution":19,"Method":"250",)" - R"("TransactionResult":"tecFROZEN"})"); - std::string const expectedJson( - R"({"CloseResolution":19,"Method":250,)" - R"("TransactionResult":"tecFROZEN"})"); - - Json::Value jv; - if (BEAST_EXPECT(parseJSONString(goodJson, jv))) - { - // Integer values are always parsed as int, - // unless they're too big. We want a small uint. - jv["CloseResolution"] = Json::UInt(19); - STParsedJSONObject parsed("test", jv); - if (BEAST_EXPECT(parsed.object)) - { - std::string const& serialized( - to_string(parsed.object->getJson(JsonOptions::none))); - BEAST_EXPECT(serialized == expectedJson); - } - } - } - - { - std::string const json(R"({"CloseResolution":19,"Method":250,)" - R"("TransactionResult":"terQUEUED"})"); - - Json::Value jv; - if (BEAST_EXPECT(parseJSONString(json, jv))) - { - STParsedJSONObject parsed("test", jv); - BEAST_EXPECT(!parsed.object); - BEAST_EXPECT(parsed.error); - BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); - BEAST_EXPECT( - parsed.error[jss::error_message] == - "Field 'test.TransactionResult' is out of range."); - } - } - - { - std::string const json(R"({"CloseResolution":19,"Method":"pony",)" - R"("TransactionResult":"tesSUCCESS"})"); - - Json::Value jv; - if (BEAST_EXPECT(parseJSONString(json, jv))) - { - STParsedJSONObject parsed("test", jv); - BEAST_EXPECT(!parsed.object); - BEAST_EXPECT(parsed.error); - BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); - BEAST_EXPECT( - parsed.error[jss::error_message] == - "Field 'test.Method' has bad type."); - } - } - - { - std::string const json( - R"({"CloseResolution":19,"Method":3294967296,)" - R"("TransactionResult":"tesSUCCESS"})"); - - Json::Value jv; - if (BEAST_EXPECT(parseJSONString(json, jv))) - { - STParsedJSONObject parsed("test", jv); - BEAST_EXPECT(!parsed.object); - BEAST_EXPECT(parsed.error); - BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); - BEAST_EXPECT( - parsed.error[jss::error_message] == - "Field 'test.Method' is out of range."); - } - } - - { - std::string const json(R"({"CloseResolution":-10,"Method":42,)" - R"("TransactionResult":"tesSUCCESS"})"); - - Json::Value jv; - if (BEAST_EXPECT(parseJSONString(json, jv))) - { - STParsedJSONObject parsed("test", jv); - BEAST_EXPECT(!parsed.object); - BEAST_EXPECT(parsed.error); - BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); - BEAST_EXPECT( - parsed.error[jss::error_message] == - "Field 'test.CloseResolution' is out of range."); - } - } - - { - std::string const json( - R"({"CloseResolution":19,"Method":3.141592653,)" - R"("TransactionResult":"tesSUCCESS"})"); - - Json::Value jv; - if (BEAST_EXPECT(parseJSONString(json, jv))) - { - STParsedJSONObject parsed("test", jv); - BEAST_EXPECT(!parsed.object); - BEAST_EXPECT(parsed.error); - BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); - BEAST_EXPECT( - parsed.error[jss::error_message] == - "Field 'test.Method' has bad type."); - } - } - } - void testSerialization() { @@ -730,9 +518,6 @@ public: testFields(); testSerialization(); - testParseJSONArray(); - testParseJSONArrayWithInvalidChildrenObjects(); - testParseJSONEdgeCases(); testMalformed(); } }; diff --git a/src/test/protocol/STParsedJSON_test.cpp b/src/test/protocol/STParsedJSON_test.cpp new file mode 100644 index 0000000000..1e1e1fb9f4 --- /dev/null +++ b/src/test/protocol/STParsedJSON_test.cpp @@ -0,0 +1,2356 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +#include +#include +#include +#include +#include +#include + +namespace ripple { + +class STParsedJSON_test : public beast::unit_test::suite +{ + bool + parseJSONString(std::string const& json, Json::Value& to) + { + Json::Reader reader; + return reader.parse(json, to) && to.isObject(); + } + + void + testUInt8() + { + testcase("UInt8"); + { + Json::Value j; + j[sfCloseResolution] = 255; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfCloseResolution)); + BEAST_EXPECT(obj.object->getFieldU8(sfCloseResolution) == 255); + } + + // test with uint value + { + Json::Value j; + j[sfCloseResolution] = 255u; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfCloseResolution)); + BEAST_EXPECT(obj.object->getFieldU8(sfCloseResolution) == 255); + } + + // Test with string value + { + Json::Value j; + j[sfCloseResolution] = "255"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfCloseResolution)); + BEAST_EXPECT(obj.object->getFieldU8(sfCloseResolution) == 255); + } + + // Test min value for uint8 + { + Json::Value j; + j[sfCloseResolution] = 0; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->getFieldU8(sfCloseResolution) == 0); + } + + // Test out of range value for UInt8 (negative) + { + Json::Value j; + j[sfCloseResolution] = -1; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test out of range value for UInt8 (too large) + { + Json::Value j; + j[sfCloseResolution] = 256; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test bad_type (not a string/int/uint) + { + Json::Value j; + j[sfCloseResolution] = Json::Value(Json::arrayValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test bad_type (not a string/int/uint) + { + Json::Value j; + j[sfCloseResolution] = Json::Value(Json::objectValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + } + + void + testUInt16() + { + testcase("UInt16"); + // Test with int value + { + Json::Value j; + j[sfLedgerEntryType] = 65535; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfLedgerEntryType)); + BEAST_EXPECT(obj.object->getFieldU16(sfLedgerEntryType) == 65535); + } + + // Test with uint value + { + Json::Value j; + j[sfLedgerEntryType] = 65535u; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfLedgerEntryType)); + BEAST_EXPECT(obj.object->getFieldU16(sfLedgerEntryType) == 65535); + } + + // Test with string value + { + Json::Value j; + j[sfLedgerEntryType] = "65535"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfLedgerEntryType)); + BEAST_EXPECT(obj.object->getFieldU16(sfLedgerEntryType) == 65535); + } + + // Test min value for uint16 + { + Json::Value j; + j[sfLedgerEntryType] = 0; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->getFieldU16(sfLedgerEntryType) == 0); + } + + // Test out of range value for UInt16 (negative) + { + Json::Value j; + j[sfLedgerEntryType] = -1; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test out of range value for UInt16 (too large) + { + Json::Value j; + j[sfLedgerEntryType] = 65536; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test string value out of range + { + Json::Value j; + j[sfLedgerEntryType] = "65536"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test bad_type (not a string/int/uint) + { + Json::Value j; + j[sfLedgerEntryType] = Json::Value(Json::arrayValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test bad_type (not a string/int/uint) + { + Json::Value j; + j[sfLedgerEntryType] = Json::Value(Json::objectValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid input for other field + { + Json::Value j; + j[sfTransferFee] = "Payment"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + } + + void + testUInt32() + { + testcase("UInt32"); + { + Json::Value j; + j[sfNetworkID] = 4294967295u; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfNetworkID)); + BEAST_EXPECT(obj.object->getFieldU32(sfNetworkID) == 4294967295u); + } + + // Test with string value + { + Json::Value j; + j[sfNetworkID] = "4294967295"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfNetworkID)); + BEAST_EXPECT(obj.object->getFieldU32(sfNetworkID) == 4294967295u); + } + + // Test min value for uint32 + { + Json::Value j; + j[sfNetworkID] = 0; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->getFieldU32(sfNetworkID) == 0); + } + + // Test out of range value for uint32 (negative) + { + Json::Value j; + j[sfNetworkID] = -1; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test string value out of range + { + Json::Value j; + j[sfNetworkID] = "4294967296"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test bad_type (arrayValue) + { + Json::Value j; + j[sfNetworkID] = Json::Value(Json::arrayValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test bad_type (objectValue) + { + Json::Value j; + j[sfNetworkID] = Json::Value(Json::objectValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + } + + void + testUInt64() + { + testcase("UInt64"); + { + Json::Value j; + j[sfIndexNext] = "ffffffffffffffff"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfIndexNext)); + BEAST_EXPECT( + obj.object->getFieldU64(sfIndexNext) == + 18446744073709551615ull); + } + + // Test min value for uint64 + { + Json::Value j; + j[sfIndexNext] = 0; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->getFieldU64(sfIndexNext) == 0ull); + } + + // Test out of range value for uint64 (negative) + { + Json::Value j; + j[sfIndexNext] = -1; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // NOTE: the JSON parser doesn't support > UInt32, so those values must + // be in hex + // Test string value out of range + // string is interpreted as hex + { + Json::Value j; + j[sfIndexNext] = "10000000000000000"; // uint64 max + 1 (in hex) + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test hex string value with 0x prefix (should fail) + { + Json::Value j; + j[sfIndexNext] = "0xabcdefabcdef"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test hex string value with invalid characters + { + Json::Value j; + j[sfIndexNext] = "abcdefga"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // test arrayValue + { + Json::Value j; + j[sfIndexNext] = Json::Value(Json::arrayValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // test objectValue + { + Json::Value j; + j[sfIndexNext] = Json::Value(Json::objectValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + } + + void + testUInt128() + { + testcase("UInt128"); + { + Json::Value j; + j[sfEmailHash] = "0123456789ABCDEF0123456789ABCDEF"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfEmailHash)); + BEAST_EXPECT(obj.object->getFieldH128(sfEmailHash).size() == 16); + std::array expected = { + 0x01, + 0x23, + 0x45, + 0x67, + 0x89, + 0xAB, + 0xCD, + 0xEF, + 0x01, + 0x23, + 0x45, + 0x67, + 0x89, + 0xAB, + 0xCD, + 0xEF}; + BEAST_EXPECT( + obj.object->getFieldH128(sfEmailHash) == uint128{expected}); + } + + // Valid lowercase hex string for UInt128 + { + Json::Value j; + j[sfEmailHash] = "0123456789abcdef0123456789abcdef"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfEmailHash)); + BEAST_EXPECT(obj.object->getFieldH128(sfEmailHash).size() == 16); + } + + // Empty string for UInt128 (should be valid, all zero) + { + Json::Value j; + j[sfEmailHash] = ""; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfEmailHash)); + auto const& h128 = obj.object->getFieldH128(sfEmailHash); + BEAST_EXPECT(h128.size() == 16); + bool allZero = std::all_of( + h128.begin(), h128.end(), [](auto b) { return b == 0; }); + BEAST_EXPECT(allZero); + } + + // Odd-length hex string for UInt128 (should fail) + { + Json::Value j; + j[sfEmailHash] = "0123456789ABCDEF0123456789ABCDE"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Non-hex string for UInt128 (should fail) + { + Json::Value j; + j[sfEmailHash] = "nothexstring"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Hex string too short for UInt128 (should fail) + { + Json::Value j; + j[sfEmailHash] = "01234567"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Hex string too long for UInt128 (should fail) + { + Json::Value j; + j[sfEmailHash] = "0123456789ABCDEF0123456789ABCDEF00"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Array value for UInt128 (should fail) + { + Json::Value j; + j[sfEmailHash] = Json::Value(Json::arrayValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Object value for UInt128 (should fail) + { + Json::Value j; + j[sfEmailHash] = Json::Value(Json::objectValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + } + + void + testUInt160() + { + testcase("UInt160"); + { + Json::Value j; + j[sfTakerPaysCurrency] = "0123456789ABCDEF0123456789ABCDEF01234567"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfTakerPaysCurrency)); + BEAST_EXPECT( + obj.object->getFieldH160(sfTakerPaysCurrency).size() == 20); + std::array expected = { + 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01, 0x23, + 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45, 0x67}; + BEAST_EXPECT( + obj.object->getFieldH160(sfTakerPaysCurrency) == + uint160{expected}); + } + // Valid lowercase hex string for UInt160 + { + Json::Value j; + j[sfTakerPaysCurrency] = "0123456789abcdef0123456789abcdef01234567"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfTakerPaysCurrency)); + BEAST_EXPECT( + obj.object->getFieldH160(sfTakerPaysCurrency).size() == 20); + } + + // Empty string for UInt160 (should be valid, all zero) + { + Json::Value j; + j[sfTakerPaysCurrency] = ""; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfTakerPaysCurrency)); + auto const& h160 = obj.object->getFieldH160(sfTakerPaysCurrency); + BEAST_EXPECT(h160.size() == 20); + bool allZero = std::all_of( + h160.begin(), h160.end(), [](auto b) { return b == 0; }); + BEAST_EXPECT(allZero); + } + + // Non-hex string for UInt160 (should fail) + { + Json::Value j; + j[sfTakerPaysCurrency] = "nothexstring"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Hex string too short for UInt160 (should fail) + { + Json::Value j; + j[sfTakerPaysCurrency] = "01234567"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Hex string too long for UInt160 (should fail) + { + Json::Value j; + j[sfTakerPaysCurrency] = + "0123456789ABCDEF0123456789ABCDEF0123456789"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Array value for UInt160 (should fail) + { + Json::Value j; + j[sfTakerPaysCurrency] = Json::Value(Json::arrayValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Object value for UInt160 (should fail) + { + Json::Value j; + j[sfTakerPaysCurrency] = Json::Value(Json::objectValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + } + + void + testUInt192() + { + testcase("UInt192"); + { + Json::Value j; + j[sfMPTokenIssuanceID] = + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfMPTokenIssuanceID)); + BEAST_EXPECT( + obj.object->getFieldH192(sfMPTokenIssuanceID).size() == 24); + std::array expected = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + BEAST_EXPECT( + obj.object->getFieldH192(sfMPTokenIssuanceID) == + uint192{expected}); + } + + // Valid lowercase hex string for UInt192 + { + Json::Value j; + j[sfMPTokenIssuanceID] = + "ffffffffffffffffffffffffffffffffffffffffffffffff"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfMPTokenIssuanceID)); + BEAST_EXPECT( + obj.object->getFieldH192(sfMPTokenIssuanceID).size() == 24); + } + + // Empty string for UInt192 (should be valid, all zero) + { + Json::Value j; + j[sfMPTokenIssuanceID] = ""; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfMPTokenIssuanceID)); + auto const& h192 = obj.object->getFieldH192(sfMPTokenIssuanceID); + BEAST_EXPECT(h192.size() == 24); + bool allZero = std::all_of( + h192.begin(), h192.end(), [](auto b) { return b == 0; }); + BEAST_EXPECT(allZero); + } + + // Odd-length hex string for UInt192 (should fail) + { + Json::Value j; + j[sfMPTokenIssuanceID] = + "0123456789ABCDEF0123456789ABCDEF0123456789ABCDE"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Non-hex string for UInt192 (should fail) + { + Json::Value j; + j[sfMPTokenIssuanceID] = "nothexstring"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Hex string too short for UInt192 (should fail) + { + Json::Value j; + j[sfMPTokenIssuanceID] = "01234567"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Hex string too long for UInt192 (should fail) + { + Json::Value j; + j[sfMPTokenIssuanceID] = + "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF00"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Array value for UInt192 (should fail) + { + Json::Value j; + j[sfMPTokenIssuanceID] = Json::Value(Json::arrayValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Object value for UInt192 (should fail) + { + Json::Value j; + j[sfMPTokenIssuanceID] = Json::Value(Json::objectValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + } + + void + testUInt256() + { + testcase("UInt256"); + // Test with valid hex string for UInt256 + { + Json::Value j; + j[sfLedgerHash] = + "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCD" + "EF"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfLedgerHash)); + BEAST_EXPECT(obj.object->getFieldH256(sfLedgerHash).size() == 32); + std::array expected = { + 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, + 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, + 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, + 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF}; + BEAST_EXPECT( + obj.object->getFieldH256(sfLedgerHash) == uint256{expected}); + } + // Valid lowercase hex string for UInt256 + { + Json::Value j; + j[sfLedgerHash] = + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcd" + "ef"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfLedgerHash)); + BEAST_EXPECT(obj.object->getFieldH256(sfLedgerHash).size() == 32); + } + + // Empty string for UInt256 (should be valid, all zero) + { + Json::Value j; + j[sfLedgerHash] = ""; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfLedgerHash)); + auto const& h256 = obj.object->getFieldH256(sfLedgerHash); + BEAST_EXPECT(h256.size() == 32); + bool allZero = std::all_of( + h256.begin(), h256.end(), [](auto b) { return b == 0; }); + BEAST_EXPECT(allZero); + } + + // Odd-length hex string for UInt256 (should fail) + { + Json::Value j; + j[sfLedgerHash] = + "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCD" + "E"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Non-hex string for UInt256 (should fail) + { + Json::Value j; + j[sfLedgerHash] = "nothexstring"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Hex string too short for UInt256 (should fail) + { + Json::Value j; + j[sfLedgerHash] = "01234567"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Hex string too long for UInt256 (should fail) + { + Json::Value j; + j[sfLedgerHash] = + "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCD" + "EF00"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Array value for UInt256 (should fail) + { + Json::Value j; + j[sfLedgerHash] = Json::Value(Json::arrayValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Object value for UInt256 (should fail) + { + Json::Value j; + j[sfLedgerHash] = Json::Value(Json::objectValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + } + + void + testInt32() + { + testcase("Int32"); + { + Json::Value j; + int const minInt32 = -2147483648; + j[sfDummyInt32] = minInt32; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + if (BEAST_EXPECT(obj.object->isFieldPresent(sfDummyInt32))) + BEAST_EXPECT(obj.object->getFieldI32(sfDummyInt32) == minInt32); + } + + // max value + { + Json::Value j; + int const maxInt32 = 2147483647; + j[sfDummyInt32] = maxInt32; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + if (BEAST_EXPECT(obj.object->isFieldPresent(sfDummyInt32))) + BEAST_EXPECT(obj.object->getFieldI32(sfDummyInt32) == maxInt32); + } + + // max uint value + { + Json::Value j; + unsigned int const maxUInt32 = 2147483647u; + j[sfDummyInt32] = maxUInt32; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + if (BEAST_EXPECT(obj.object->isFieldPresent(sfDummyInt32))) + BEAST_EXPECT( + obj.object->getFieldI32(sfDummyInt32) == + static_cast(maxUInt32)); + } + + // Test with string value + { + Json::Value j; + j[sfDummyInt32] = "2147483647"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + if (BEAST_EXPECT(obj.object->isFieldPresent(sfDummyInt32))) + BEAST_EXPECT( + obj.object->getFieldI32(sfDummyInt32) == 2147483647u); + } + + // Test with string negative value + { + Json::Value j; + int value = -2147483648; + j[sfDummyInt32] = std::to_string(value); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + if (BEAST_EXPECT(obj.object->isFieldPresent(sfDummyInt32))) + BEAST_EXPECT(obj.object->getFieldI32(sfDummyInt32) == value); + } + + // Test out of range value for int32 (negative) + { + Json::Value j; + j[sfDummyInt32] = "-2147483649"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test out of range value for int32 (positive) + { + Json::Value j; + j[sfDummyInt32] = 2147483648u; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test string value out of range + { + Json::Value j; + j[sfDummyInt32] = "2147483648"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test bad_type (arrayValue) + { + Json::Value j; + j[sfDummyInt32] = Json::Value(Json::arrayValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test bad_type (objectValue) + { + Json::Value j; + j[sfDummyInt32] = Json::Value(Json::objectValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + } + + void + testBlob() + { + testcase("Blob"); + // Test with valid hex string for blob + { + Json::Value j; + j[sfPublicKey] = "DEADBEEF"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfPublicKey)); + auto const& blob = obj.object->getFieldVL(sfPublicKey); + BEAST_EXPECT(blob.size() == 4); + BEAST_EXPECT(blob[0] == 0xDE); + BEAST_EXPECT(blob[1] == 0xAD); + BEAST_EXPECT(blob[2] == 0xBE); + BEAST_EXPECT(blob[3] == 0xEF); + } + + // Test empty string for blob (should be valid, size 0) + { + Json::Value j; + j[sfPublicKey] = ""; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfPublicKey)); + auto const& blob = obj.object->getFieldVL(sfPublicKey); + BEAST_EXPECT(blob.size() == 0); + } + + // Test lowercase hex string for blob + { + Json::Value j; + j[sfPublicKey] = "deadbeef"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfPublicKey)); + auto const& blob = obj.object->getFieldVL(sfPublicKey); + BEAST_EXPECT(blob.size() == 4); + BEAST_EXPECT(blob[0] == 0xDE); + BEAST_EXPECT(blob[1] == 0xAD); + BEAST_EXPECT(blob[2] == 0xBE); + BEAST_EXPECT(blob[3] == 0xEF); + } + + // Test non-hex string for blob (should fail) + { + Json::Value j; + j[sfPublicKey] = "XYZ123"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test array value for blob (should fail) + { + Json::Value j; + j[sfPublicKey] = Json::Value(Json::arrayValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test object value for blob (should fail) + { + Json::Value j; + j[sfPublicKey] = Json::Value(Json::objectValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + } + + void + testVector256() + { + testcase("Vector256"); + // Test with valid array of hex strings for Vector256 + { + Json::Value j; + Json::Value arr(Json::arrayValue); + arr.append( + "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCD" + "EF"); + arr.append( + "FEDCBA9876543210FEDCBA9876543210FEDCBA9876543210FEDCBA98765432" + "10"); + j[sfHashes] = arr; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfHashes)); + auto const& vec = obj.object->getFieldV256(sfHashes); + BEAST_EXPECT(vec.size() == 2); + BEAST_EXPECT(to_string(vec[0]) == arr[0u].asString()); + BEAST_EXPECT(to_string(vec[1]) == arr[1u].asString()); + } + // Test empty array for Vector256 (should be valid, size 0) + { + Json::Value j; + Json::Value arr(Json::arrayValue); + j[sfHashes] = arr; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfHashes)); + auto const& vec = obj.object->getFieldV256(sfHashes); + BEAST_EXPECT(vec.size() == 0); + } + + // Test array with invalid hex string (should fail) + { + Json::Value j; + Json::Value arr(Json::arrayValue); + arr.append("nothexstring"); + j[sfHashes] = arr; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test array with string of wrong length (should fail) + { + Json::Value j; + Json::Value arr(Json::arrayValue); + arr.append("0123456789ABCDEF"); // too short for uint256 + j[sfHashes] = arr; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test array with non-string element (should fail) + { + Json::Value j; + Json::Value arr(Json::arrayValue); + arr.append(12345); + j[sfHashes] = arr; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test non-array value for Vector256 (should fail) + { + Json::Value j; + j[sfHashes] = "notanarray"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test array with object element (should fail) + { + Json::Value j; + Json::Value arr(Json::arrayValue); + Json::Value objElem(Json::objectValue); + objElem["foo"] = "bar"; + arr.append(objElem); + j[sfHashes] = arr; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + } + + void + testAccount() + { + testcase("Account"); + // Test with valid base58 string for AccountID + { + Json::Value j; + j[sfAccount] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfAccount)); + auto const& acct = obj.object->getAccountID(sfAccount); + BEAST_EXPECT(acct.size() == 20); + BEAST_EXPECT( + toBase58(acct) == "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"); + } + + // Valid hex string for AccountID + { + Json::Value j; + j[sfAccount] = "000102030405060708090A0B0C0D0E0F10111213"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfAccount)); + auto const& acct = obj.object->getAccountID(sfAccount); + BEAST_EXPECT(acct.size() == 20); + } + + // Invalid base58 string for AccountID + { + Json::Value j; + j[sfAccount] = "notAValidBase58Account"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid hex string for AccountID (too short) + { + Json::Value j; + j[sfAccount] = "001122334455"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid hex string for AccountID (too long) + { + Json::Value j; + j[sfAccount] = "000102030405060708090A0B0C0D0E0F101112131415"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid hex string for AccountID (bad chars) + { + Json::Value j; + j[sfAccount] = "000102030405060708090A0B0C0D0E0F1011121G"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Empty string for AccountID (should fail) + { + Json::Value j; + j[sfAccount] = ""; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Array value for AccountID (should fail) + { + Json::Value j; + j[sfAccount] = Json::Value(Json::arrayValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Object value for AccountID (should fail) + { + Json::Value j; + j[sfAccount] = Json::Value(Json::objectValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + } + + void + testCurrency() + { + testcase("Currency"); + // Test with valid ISO code for currency + { + Json::Value j; + j[sfBaseAsset] = "USD"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfBaseAsset)); + auto const& curr = obj.object->getFieldCurrency(sfBaseAsset); + BEAST_EXPECT(curr.currency().size() == 20); + } + + // Valid ISO code + { + Json::Value j; + j[sfBaseAsset] = "EUR"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfBaseAsset)); + auto const& curr = obj.object->getFieldCurrency(sfBaseAsset); + BEAST_EXPECT(curr.currency().size() == 20); + } + + // Valid hex string for currency + { + Json::Value j; + j[sfBaseAsset] = "0123456789ABCDEF01230123456789ABCDEF0123"; + STParsedJSONObject obj("Test", j); + if (BEAST_EXPECT(obj.object.has_value())) + { + BEAST_EXPECT(obj.object->isFieldPresent(sfBaseAsset)); + auto const& curr = obj.object->getFieldCurrency(sfBaseAsset); + BEAST_EXPECT(curr.currency().size() == 20); + } + } + + // Invalid ISO code (too long) + { + Json::Value j; + j[sfBaseAsset] = "USDD"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // lowercase ISO code + { + Json::Value j; + j[sfBaseAsset] = "usd"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfBaseAsset)); + auto const& curr = obj.object->getFieldCurrency(sfBaseAsset); + BEAST_EXPECT(curr.currency().size() == 20); + } + + // Invalid hex string (too short) + { + Json::Value j; + j[sfBaseAsset] = "0123456789AB"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid hex string (too long) + { + Json::Value j; + j[sfBaseAsset] = "0123456789ABCDEF0123456789"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Empty string for currency (should fail) + { + Json::Value j; + j[sfBaseAsset] = ""; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfBaseAsset)); + auto const& curr = obj.object->getFieldCurrency(sfBaseAsset); + BEAST_EXPECT(curr.currency().size() == 20); + } + + // Array value for currency (should fail) + { + Json::Value j; + j[sfBaseAsset] = Json::Value(Json::arrayValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Object value for currency (should fail) + { + Json::Value j; + j[sfBaseAsset] = Json::Value(Json::objectValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + } + + void + testAmount() + { + testcase("Amount"); + // Test with string value for Amount + { + Json::Value j; + j[sfAmount] = "100000000000000000"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfAmount)); + BEAST_EXPECT( + obj.object->getFieldAmount(sfAmount) == + STAmount(100000000000000000ull)); + } + + // Test with int value for Amount + { + Json::Value j; + j[sfAmount] = 4294967295u; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfAmount)); + BEAST_EXPECT( + obj.object->getFieldAmount(sfAmount) == STAmount(4294967295u)); + } + + // Test with decimal string for Amount (should fail) + { + Json::Value j; + j[sfAmount] = "123.45"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with empty string for Amount (should fail) + { + Json::Value j; + j[sfAmount] = ""; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with non-numeric string for Amount (should fail) + { + Json::Value j; + j[sfAmount] = "notanumber"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with object value for Amount (should fail) + { + Json::Value j; + j[sfAmount] = Json::Value(Json::objectValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + } + + void + testPathSet() + { + testcase("PathSet"); + // Valid test: single path with single element + { + Json::Value j; + Json::Value path(Json::arrayValue); + Json::Value elem(Json::objectValue); + elem["account"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + elem["currency"] = "USD"; + elem["issuer"] = "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe"; + path.append(elem); + Json::Value pathset(Json::arrayValue); + pathset.append(path); + j[sfPaths] = pathset; + STParsedJSONObject obj("Test", j); + if (BEAST_EXPECT(obj.object.has_value())) + { + BEAST_EXPECT(obj.object->isFieldPresent(sfPaths)); + auto const& ps = obj.object->getFieldPathSet(sfPaths); + BEAST_EXPECT(!ps.empty()); + BEAST_EXPECT(ps.size() == 1); + BEAST_EXPECT(ps[0].size() == 1); + BEAST_EXPECT( + ps[0][0].getAccountID() == + parseBase58( + "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh")); + BEAST_EXPECT(to_string(ps[0][0].getCurrency()) == "USD"); + BEAST_EXPECT( + ps[0][0].getIssuerID() == + parseBase58( + "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe")); + } + } + + // Valid test: non-standard currency code + { + Json::Value j; + Json::Value path(Json::arrayValue); + Json::Value elem(Json::objectValue); + elem["account"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + elem["currency"] = "0123456789ABCDEF01230123456789ABCDEF0123"; + elem["issuer"] = "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe"; + path.append(elem); + Json::Value pathset(Json::arrayValue); + pathset.append(path); + j[sfPaths] = pathset; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfPaths)); + auto const& ps = obj.object->getFieldPathSet(sfPaths); + BEAST_EXPECT(!ps.empty()); + } + + // Test with non-array value for PathSet (should fail) + { + Json::Value j; + j[sfPaths] = "notanarray"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with array containing non-array element (should fail) + { + Json::Value j; + Json::Value pathset(Json::arrayValue); + pathset.append("notanarray"); + j[sfPaths] = pathset; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with array containing array with non-object element (should + // fail) + { + Json::Value j; + Json::Value path(Json::arrayValue); + path.append("notanobject"); + Json::Value pathset(Json::arrayValue); + pathset.append(path); + j[sfPaths] = pathset; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with array containing array with object missing required keys + // (should fail) + { + Json::Value j; + Json::Value path(Json::arrayValue); + Json::Value elem(Json::objectValue); + elem["foo"] = "bar"; // not a valid path element key + path.append(elem); + Json::Value pathset(Json::arrayValue); + pathset.append(path); + j[sfPaths] = pathset; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with array containing array with object with invalid account + // value (should fail) + { + Json::Value j; + Json::Value path(Json::arrayValue); + Json::Value elem(Json::objectValue); + elem["account"] = "notAValidBase58Account"; + path.append(elem); + Json::Value pathset(Json::arrayValue); + pathset.append(path); + j[sfPaths] = pathset; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with account not string (should fail) + { + Json::Value j; + Json::Value path(Json::arrayValue); + Json::Value elem(Json::objectValue); + elem["account"] = 12345; + path.append(elem); + Json::Value pathset(Json::arrayValue); + pathset.append(path); + j[sfPaths] = pathset; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with currency not string (should fail) + { + Json::Value j; + Json::Value path(Json::arrayValue); + Json::Value elem(Json::objectValue); + elem["currency"] = 12345; + path.append(elem); + Json::Value pathset(Json::arrayValue); + pathset.append(path); + j[sfPaths] = pathset; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with non-standard currency not hex (should fail) + { + Json::Value j; + Json::Value path(Json::arrayValue); + Json::Value elem(Json::objectValue); + elem["currency"] = "notAValidCurrency"; + path.append(elem); + Json::Value pathset(Json::arrayValue); + pathset.append(path); + j[sfPaths] = pathset; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with issuer not string (should fail) + { + Json::Value j; + Json::Value path(Json::arrayValue); + Json::Value elem(Json::objectValue); + elem["issuer"] = 12345; + path.append(elem); + Json::Value pathset(Json::arrayValue); + pathset.append(path); + j[sfPaths] = pathset; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with issuer not base58 (should fail) + { + Json::Value j; + Json::Value path(Json::arrayValue); + Json::Value elem(Json::objectValue); + elem["issuer"] = "notAValidBase58Account"; + path.append(elem); + Json::Value pathset(Json::arrayValue); + pathset.append(path); + j[sfPaths] = pathset; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + } + + void + testIssue() + { + testcase("Issue"); + // Valid Issue: currency and issuer as base58 + { + Json::Value j; + Json::Value issueJson(Json::objectValue); + issueJson["currency"] = "USD"; + issueJson["issuer"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + j[sfAsset] = issueJson; + STParsedJSONObject obj("Test", j); + if (BEAST_EXPECT(obj.object.has_value())) + { + BEAST_EXPECT(obj.object->isFieldPresent(sfAsset)); + auto const& issueField = (*obj.object)[sfAsset]; + auto const issue = issueField.value().get(); + BEAST_EXPECT(issue.currency.size() == 20); + BEAST_EXPECT(to_string(issue.currency) == "USD"); + BEAST_EXPECT(issue.account.size() == 20); + BEAST_EXPECT( + issue.account == + parseBase58( + "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh")); + } + } + + // Valid Issue: currency as hex + { + Json::Value j; + Json::Value issueJson(Json::objectValue); + issueJson["currency"] = "0123456789ABCDEF01230123456789ABCDEF0123"; + issueJson["issuer"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + j[sfAsset] = issueJson; + STParsedJSONObject obj("Test", j); + if (BEAST_EXPECT(obj.object.has_value())) + { + BEAST_EXPECT(obj.object->isFieldPresent(sfAsset)); + auto const& issueField = (*obj.object)[sfAsset]; + auto const issue = issueField.value().get(); + BEAST_EXPECT(issue.currency.size() == 20); + BEAST_EXPECT(issue.account.size() == 20); + } + } + + // Valid Issue: MPTID + { + Json::Value j; + Json::Value issueJson(Json::objectValue); + issueJson["mpt_issuance_id"] = + "0000000000000000000000004D5054494431323334234234"; + j[sfAsset] = issueJson; + STParsedJSONObject obj("Test", j); + if (BEAST_EXPECT(obj.object.has_value())) + { + BEAST_EXPECT(obj.object->isFieldPresent(sfAsset)); + auto const& issueField = (*obj.object)[sfAsset]; + auto const issue = issueField.value().get(); + BEAST_EXPECT(issue.getMptID().size() == 24); + } + } + + // Invalid Issue: missing currency + { + Json::Value j; + Json::Value issue(Json::objectValue); + issue["issuer"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + j[sfAsset] = issue; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid Issue: missing issuer + { + Json::Value j; + Json::Value issue(Json::objectValue); + issue["currency"] = "USD"; + j[sfAsset] = issue; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid Issue: currency too long + { + Json::Value j; + Json::Value issue(Json::objectValue); + issue["currency"] = "USDD"; + issue["issuer"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + j[sfAsset] = issue; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid Issue: issuer not base58 or hex + { + Json::Value j; + Json::Value issue(Json::objectValue); + issue["currency"] = "USD"; + issue["issuer"] = "notAValidIssuer"; + j[sfAsset] = issue; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid Issue: currency not string + { + Json::Value j; + Json::Value issue(Json::objectValue); + issue["currency"] = Json::Value(Json::arrayValue); + issue["issuer"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + j[sfAsset] = issue; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid Issue: issuer not string + { + Json::Value j; + Json::Value issue(Json::objectValue); + issue["currency"] = "USD"; + issue["issuer"] = Json::Value(Json::objectValue); + j[sfAsset] = issue; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid Issue: not an object + { + Json::Value j; + j[sfAsset] = "notanobject"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + } + + void + testXChainBridge() + { + testcase("XChainBridge"); + // Valid XChainBridge + { + Json::Value j; + Json::Value bridge(Json::objectValue); + Json::Value issuingChainIssue(Json::objectValue); + issuingChainIssue["currency"] = "USD"; + issuingChainIssue["issuer"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + Json::Value lockingChainIssue(Json::objectValue); + lockingChainIssue["currency"] = "EUR"; + lockingChainIssue["issuer"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + bridge["LockingChainIssue"] = lockingChainIssue; + bridge["IssuingChainIssue"] = issuingChainIssue; + bridge["LockingChainDoor"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + bridge["IssuingChainDoor"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + j[sfXChainBridge] = bridge; + STParsedJSONObject obj("Test", j); + if (BEAST_EXPECT(obj.object.has_value())) + { + BEAST_EXPECT(obj.object->isFieldPresent(sfXChainBridge)); + auto const& bridgeField = (*obj.object)[sfXChainBridge]; + BEAST_EXPECT( + bridgeField->lockingChainIssue().currency.size() == 20); + BEAST_EXPECT( + bridgeField->issuingChainIssue().currency.size() == 20); + } + } + + // Valid XChainBridge: issues as hex currency + { + Json::Value j; + Json::Value bridge(Json::objectValue); + Json::Value issuingChainIssue(Json::objectValue); + issuingChainIssue["currency"] = + "0123456789ABCDEF01230123456789ABCDEF0123"; + issuingChainIssue["issuer"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + Json::Value lockingChainIssue(Json::objectValue); + lockingChainIssue["currency"] = + "0123456789ABCDEF01230123456789ABCDEF0123"; + lockingChainIssue["issuer"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + bridge["LockingChainIssue"] = lockingChainIssue; + bridge["IssuingChainIssue"] = issuingChainIssue; + bridge["LockingChainDoor"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + bridge["IssuingChainDoor"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + j[sfXChainBridge] = bridge; + STParsedJSONObject obj("Test", j); + if (BEAST_EXPECT(obj.object.has_value())) + { + BEAST_EXPECT(obj.object->isFieldPresent(sfXChainBridge)); + auto const& bridgeField = (*obj.object)[sfXChainBridge]; + BEAST_EXPECT( + bridgeField->lockingChainIssue().currency.size() == 20); + BEAST_EXPECT( + bridgeField->issuingChainIssue().currency.size() == 20); + } + } + + // Invalid XChainBridge: missing LockingChainIssue + { + Json::Value j; + Json::Value bridge(Json::objectValue); + Json::Value issuingChainIssue(Json::objectValue); + issuingChainIssue["currency"] = "USD"; + issuingChainIssue["issuer"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + bridge["IssuingChainIssue"] = issuingChainIssue; + bridge["LockingChainDoor"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + bridge["IssuingChainDoor"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + j[sfXChainBridge] = bridge; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid XChainBridge: missing IssuingChainIssue + { + Json::Value j; + Json::Value bridge(Json::objectValue); + Json::Value lockingChainIssue(Json::objectValue); + lockingChainIssue["currency"] = "EUR"; + lockingChainIssue["issuer"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + bridge["LockingChainIssue"] = lockingChainIssue; + bridge["LockingChainDoor"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + bridge["IssuingChainDoor"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + j[sfXChainBridge] = bridge; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid XChainBridge: missing LockingChainDoor + { + Json::Value j; + Json::Value bridge(Json::objectValue); + Json::Value issuingChainIssue(Json::objectValue); + issuingChainIssue["currency"] = "USD"; + issuingChainIssue["issuer"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + bridge["IssuingChainIssue"] = issuingChainIssue; + Json::Value lockingChainIssue(Json::objectValue); + lockingChainIssue["currency"] = "EUR"; + lockingChainIssue["issuer"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + bridge["LockingChainIssue"] = lockingChainIssue; + bridge["IssuingChainDoor"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + j[sfXChainBridge] = bridge; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid XChainBridge: missing IssuingChainDoor + { + Json::Value j; + Json::Value bridge(Json::objectValue); + Json::Value issuingChainIssue(Json::objectValue); + issuingChainIssue["currency"] = "USD"; + issuingChainIssue["issuer"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + bridge["IssuingChainIssue"] = issuingChainIssue; + Json::Value lockingChainIssue(Json::objectValue); + lockingChainIssue["currency"] = "EUR"; + lockingChainIssue["issuer"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + bridge["LockingChainIssue"] = lockingChainIssue; + bridge["LockingChainDoor"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + j[sfXChainBridge] = bridge; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid XChainBridge: IssuingChainIssue not an object + { + Json::Value j; + Json::Value bridge(Json::objectValue); + bridge["LockingChainIssue"] = "notanobject"; + bridge["IssuingChainIssue"] = "notanobject"; + j[sfXChainBridge] = bridge; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid XChainBridge: IssuingChainIssue missing currency + { + Json::Value j; + Json::Value bridge(Json::objectValue); + Json::Value asset(Json::objectValue); + asset["issuer"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + Json::Value lockingChainIssue(Json::objectValue); + lockingChainIssue["currency"] = "EUR"; + lockingChainIssue["issuer"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + bridge["LockingChainIssue"] = lockingChainIssue; + bridge["IssuingChainIssue"] = asset; + j[sfXChainBridge] = bridge; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid XChainBridge: asset missing issuer + { + Json::Value j; + Json::Value bridge(Json::objectValue); + Json::Value asset(Json::objectValue); + asset["currency"] = "USD"; + Json::Value lockingChainIssue(Json::objectValue); + lockingChainIssue["currency"] = "EUR"; + lockingChainIssue["issuer"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + bridge["LockingChainIssue"] = lockingChainIssue; + bridge["IssuingChainIssue"] = asset; + j[sfXChainBridge] = bridge; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid XChainBridge: asset issuer not base58 + { + Json::Value j; + Json::Value bridge(Json::objectValue); + Json::Value asset(Json::objectValue); + asset["currency"] = "USD"; + asset["issuer"] = "notAValidBase58Account"; + Json::Value lockingChainIssue(Json::objectValue); + lockingChainIssue["currency"] = "EUR"; + lockingChainIssue["issuer"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + bridge["LockingChainIssue"] = lockingChainIssue; + bridge["IssuingChainIssue"] = asset; + j[sfXChainBridge] = bridge; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid XChainBridge: not an object + { + Json::Value j; + j[sfXChainBridge] = "notanobject"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + } + + void + testNumber() + { + testcase("Number"); + // Valid integer value for STNumber + { + Json::Value j; + j[sfNumber] = 12345; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfNumber)); + BEAST_EXPECT( + obj.object->getFieldNumber(sfNumber).value() == + Number(12345, 0)); + } + + // Valid uint value for STNumber + { + Json::Value j; + j[sfNumber] = 12345u; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfNumber)); + BEAST_EXPECT( + obj.object->getFieldNumber(sfNumber).value() == + Number(12345, 0)); + } + + // Valid string integer value for STNumber + { + Json::Value j; + j[sfNumber] = "67890"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfNumber)); + BEAST_EXPECT( + obj.object->getFieldNumber(sfNumber).value() == + Number(67890, 0)); + } + + // Valid negative integer value for STNumber + { + Json::Value j; + j[sfNumber] = -42; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfNumber)); + BEAST_EXPECT( + obj.object->getFieldNumber(sfNumber).value() == Number(-42, 0)); + } + + // Valid string negative integer value for STNumber + { + Json::Value j; + j[sfNumber] = "-123"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfNumber)); + BEAST_EXPECT( + obj.object->getFieldNumber(sfNumber).value() == + Number(-123, 0)); + } + + // Valid floating point value for STNumber + { + Json::Value j; + j[sfNumber] = "3.14159"; + STParsedJSONObject obj("Test", j); + if (BEAST_EXPECT(obj.object.has_value())) + { + BEAST_EXPECT(obj.object->isFieldPresent(sfNumber)); + BEAST_EXPECT( + obj.object->getFieldNumber(sfNumber).value() == + Number(314159, -5)); + } + } + + // Invalid string value for STNumber (not a number) + { + Json::Value j; + j[sfNumber] = "notanumber"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid array value for STNumber + { + Json::Value j; + j[sfNumber] = Json::Value(Json::arrayValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid object value for STNumber + { + Json::Value j; + j[sfNumber] = Json::Value(Json::objectValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Empty string for STNumber (should fail) + { + Json::Value j; + j[sfNumber] = ""; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + } + + void + testObject() + { + testcase("Object"); + // Test with valid object for Object + { + Json::Value j; + Json::Value objVal(Json::objectValue); + objVal[sfTransactionResult] = 1; + j[sfTransactionMetaData] = objVal; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfTransactionMetaData)); + auto const& result = + obj.object->peekFieldObject(sfTransactionMetaData); + BEAST_EXPECT(result.getFieldU8(sfTransactionResult) == 1); + } + + // Test with non-object value for Object (should fail) + { + Json::Value j; + j[sfTransactionMetaData] = "notanobject"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with array value for Object (should fail) + { + Json::Value j; + Json::Value arr(Json::arrayValue); + arr.append(1); + j[sfTransactionMetaData] = arr; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with null value for Object (should fail) + { + Json::Value j; + j[sfTransactionMetaData] = Json::Value(Json::nullValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with max depth (should succeed) + // max depth is 64 + { + Json::Value j; + Json::Value obj(Json::objectValue); + Json::Value* current = &obj; + for (int i = 0; i < 63; ++i) + { + Json::Value next(Json::objectValue); + (*current)[sfTransactionMetaData] = next; + current = &((*current)[sfTransactionMetaData]); + } + (*current)[sfTransactionResult.getJsonName()] = 1; + j[sfTransactionMetaData] = obj; + STParsedJSONObject parsed("Test", j); + BEAST_EXPECT(parsed.object.has_value()); + BEAST_EXPECT(parsed.object->isFieldPresent(sfTransactionMetaData)); + } + + // Test with depth exceeding maxDepth (should fail) + { + Json::Value j; + Json::Value obj(Json::objectValue); + Json::Value* current = &obj; + for (int i = 0; i < 64; ++i) + { + Json::Value next(Json::objectValue); + (*current)[sfTransactionMetaData] = next; + current = &((*current)[sfTransactionMetaData]); + } + (*current)[sfTransactionResult.getJsonName()] = 1; + j[sfTransactionMetaData] = obj; + STParsedJSONObject parsed("Test", j); + BEAST_EXPECT(!parsed.object.has_value()); + } + } + + void + testArray() + { + testcase("Array"); + // Test with valid array for Array + { + Json::Value j; + Json::Value arr(Json::arrayValue); + Json::Value elem(Json::objectValue); + elem[sfTransactionResult] = 2; + Json::Value elem2(Json::objectValue); + elem2[sfTransactionMetaData] = elem; + arr.append(elem2); + j[sfSignerEntries] = arr; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfSignerEntries)); + auto const& result = obj.object->getFieldArray(sfSignerEntries); + if (BEAST_EXPECT(result.size() == 1)) + { + BEAST_EXPECT(result[0].getFName() == sfTransactionMetaData); + BEAST_EXPECT(result[0].getJson(0) == elem); + } + } + + // Test with array containing non-object element (should fail) + { + Json::Value j; + Json::Value arr(Json::arrayValue); + arr.append("notanobject"); + j[sfSignerEntries] = arr; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with array containing object with invalid field (should fail) + { + Json::Value j; + Json::Value arr(Json::arrayValue); + Json::Value elem(Json::objectValue); + elem["invalidField"] = 1; + arr.append(elem); + j[sfSignerEntries] = arr; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with array containing object with multiple keys (should fail) + { + Json::Value j; + Json::Value arr(Json::arrayValue); + Json::Value elem(Json::objectValue); + elem[sfTransactionResult] = 2; + elem[sfNetworkID] = 3; + arr.append(elem); + j[sfSignerEntries] = arr; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with non-array value for Array (should fail) + { + Json::Value j; + j[sfSignerEntries] = "notanarray"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with array containing object with valid field but invalid value + // (should fail) + { + Json::Value j; + Json::Value arr(Json::arrayValue); + Json::Value elem(Json::objectValue); + elem[sfTransactionResult] = "notanint"; + arr.append(elem); + j[sfSignerEntries] = arr; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with empty array for Array (should be valid) + { + Json::Value j; + Json::Value arr(Json::arrayValue); + j[sfSignerEntries] = arr; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfSignerEntries)); + } + + // Test with object provided but not object SField + { + Json::Value j; + Json::Value obj(Json::arrayValue); + obj.append(Json::Value(Json::objectValue)); + obj[0u][sfTransactionResult] = 1; + j[sfSignerEntries] = obj; + STParsedJSONObject parsed("Test", j); + BEAST_EXPECT(!parsed.object.has_value()); + } + + // Test invalid children + { + try + { + /* + + STArray/STObject constructs don't really map perfectly to json + arrays/objects. + + STObject is an associative container, mapping fields to value, + but an STObject may also have a Field as its name, stored + outside the associative structure. The name is important, so to + maintain fidelity, it will take TWO json objects to represent + them. + + */ + std::string faulty( + "{\"Template\":[{" + "\"ModifiedNode\":{\"Sequence\":1}, " + "\"DeletedNode\":{\"Sequence\":1}" + "}]}"); + + std::unique_ptr so; + Json::Value faultyJson; + bool parsedOK(parseJSONString(faulty, faultyJson)); + unexpected(!parsedOK, "failed to parse"); + STParsedJSONObject parsed("test", faultyJson); + BEAST_EXPECT(!parsed.object); + } + catch (std::runtime_error& e) + { + std::string what(e.what()); + unexpected( + what.find("First level children of `Template`") != 0); + } + } + } + + void + testEdgeCases() + { + testcase("General Invalid Cases"); + + { + Json::Value j; + j[sfLedgerEntry] = 1; // not a valid SField for STParsedJSON + } + + { + std::string const goodJson(R"({"CloseResolution":19,"Method":250,)" + R"("TransactionResult":"tecFROZEN"})"); + + Json::Value jv; + if (BEAST_EXPECT(parseJSONString(goodJson, jv))) + { + STParsedJSONObject parsed("test", jv); + if (BEAST_EXPECT(parsed.object)) + { + std::string const& serialized( + to_string(parsed.object->getJson(JsonOptions::none))); + BEAST_EXPECT(serialized == goodJson); + } + } + } + + { + std::string const goodJson( + R"({"CloseResolution":19,"Method":"250",)" + R"("TransactionResult":"tecFROZEN"})"); + std::string const expectedJson( + R"({"CloseResolution":19,"Method":250,)" + R"("TransactionResult":"tecFROZEN"})"); + + Json::Value jv; + if (BEAST_EXPECT(parseJSONString(goodJson, jv))) + { + // Integer values are always parsed as int, + // unless they're too big. We want a small uint. + jv["CloseResolution"] = Json::UInt(19); + STParsedJSONObject parsed("test", jv); + if (BEAST_EXPECT(parsed.object)) + { + std::string const& serialized( + to_string(parsed.object->getJson(JsonOptions::none))); + BEAST_EXPECT(serialized == expectedJson); + } + } + } + + { + std::string const goodJson( + R"({"CloseResolution":"19","Method":"250",)" + R"("TransactionResult":"tecFROZEN"})"); + std::string const expectedJson( + R"({"CloseResolution":19,"Method":250,)" + R"("TransactionResult":"tecFROZEN"})"); + + Json::Value jv; + if (BEAST_EXPECT(parseJSONString(goodJson, jv))) + { + // Integer values are always parsed as int, + // unless they're too big. We want a small uint. + jv["CloseResolution"] = Json::UInt(19); + STParsedJSONObject parsed("test", jv); + if (BEAST_EXPECT(parsed.object)) + { + std::string const& serialized( + to_string(parsed.object->getJson(JsonOptions::none))); + BEAST_EXPECT(serialized == expectedJson); + } + } + } + + { + std::string const json(R"({"CloseResolution":19,"Method":250,)" + R"("TransactionResult":"terQUEUED"})"); + + Json::Value jv; + if (BEAST_EXPECT(parseJSONString(json, jv))) + { + STParsedJSONObject parsed("test", jv); + BEAST_EXPECT(!parsed.object); + BEAST_EXPECT(parsed.error); + BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); + BEAST_EXPECT( + parsed.error[jss::error_message] == + "Field 'test.TransactionResult' is out of range."); + } + } + + { + std::string const json(R"({"CloseResolution":19,"Method":"pony",)" + R"("TransactionResult":"tesSUCCESS"})"); + + Json::Value jv; + if (BEAST_EXPECT(parseJSONString(json, jv))) + { + STParsedJSONObject parsed("test", jv); + BEAST_EXPECT(!parsed.object); + BEAST_EXPECT(parsed.error); + BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); + BEAST_EXPECT( + parsed.error[jss::error_message] == + "Field 'test.Method' has bad type."); + } + } + + { + std::string const json( + R"({"CloseResolution":19,"Method":3294967296,)" + R"("TransactionResult":"tesSUCCESS"})"); + + Json::Value jv; + if (BEAST_EXPECT(parseJSONString(json, jv))) + { + STParsedJSONObject parsed("test", jv); + BEAST_EXPECT(!parsed.object); + BEAST_EXPECT(parsed.error); + BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); + BEAST_EXPECT( + parsed.error[jss::error_message] == + "Field 'test.Method' is out of range."); + } + } + + { + std::string const json(R"({"CloseResolution":-10,"Method":42,)" + R"("TransactionResult":"tesSUCCESS"})"); + + Json::Value jv; + if (BEAST_EXPECT(parseJSONString(json, jv))) + { + STParsedJSONObject parsed("test", jv); + BEAST_EXPECT(!parsed.object); + BEAST_EXPECT(parsed.error); + BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); + BEAST_EXPECT( + parsed.error[jss::error_message] == + "Field 'test.CloseResolution' is out of range."); + } + } + + { + std::string const json( + R"({"CloseResolution":19,"Method":3.141592653,)" + R"("TransactionResult":"tesSUCCESS"})"); + + Json::Value jv; + if (BEAST_EXPECT(parseJSONString(json, jv))) + { + STParsedJSONObject parsed("test", jv); + BEAST_EXPECT(!parsed.object); + BEAST_EXPECT(parsed.error); + BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); + BEAST_EXPECT( + parsed.error[jss::error_message] == + "Field 'test.Method' has bad type."); + } + } + + { + std::string const goodJson(R"({"CloseResolution":19,"Method":250,)" + R"("TransferFee":"65535"})"); + std::string const expectedJson( + R"({"CloseResolution":19,"Method":250,)" + R"("TransferFee":65535})"); + + Json::Value jv; + if (BEAST_EXPECT(parseJSONString(goodJson, jv))) + { + STParsedJSONObject parsed("test", jv); + if (BEAST_EXPECT(parsed.object)) + { + std::string const& serialized( + to_string(parsed.object->getJson(JsonOptions::none))); + BEAST_EXPECT(serialized == expectedJson); + } + } + } + + { + std::string const json(R"({"CloseResolution":19,"Method":250,)" + R"("TransferFee":"65536"})"); + + Json::Value jv; + if (BEAST_EXPECT(parseJSONString(json, jv))) + { + STParsedJSONObject parsed("test", jv); + BEAST_EXPECT(!parsed.object); + BEAST_EXPECT(parsed.error); + BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); + BEAST_EXPECT( + parsed.error[jss::error_message] == + "Field 'test.TransferFee' has invalid data."); + } + } + + { + std::string const json(R"({"CloseResolution":19,"Method":250,)" + R"("TransferFee":"Payment"})"); + + Json::Value jv; + if (BEAST_EXPECT(parseJSONString(json, jv))) + { + STParsedJSONObject parsed("test", jv); + BEAST_EXPECT(!parsed.object); + BEAST_EXPECT(parsed.error); + BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); + BEAST_EXPECT( + parsed.error[jss::error_message] == + "Field 'test.TransferFee' has invalid data."); + } + } + + { + std::string const json(R"({"CloseResolution":19,"Method":250,)" + R"("TransferFee":true})"); + + Json::Value jv; + if (BEAST_EXPECT(parseJSONString(json, jv))) + { + STParsedJSONObject parsed("test", jv); + BEAST_EXPECT(!parsed.object); + BEAST_EXPECT(parsed.error); + BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); + BEAST_EXPECT( + parsed.error[jss::error_message] == + "Field 'test.TransferFee' has bad type."); + } + } + } + + void + run() override + { + // Instantiate a jtx::Env so debugLog writes are exercised. + test::jtx::Env env(*this); + testUInt8(); + testUInt16(); + testUInt32(); + testUInt64(); + testUInt128(); + testUInt160(); + testUInt192(); + testUInt256(); + testInt32(); + testBlob(); + testVector256(); + testAccount(); + testCurrency(); + testAmount(); + testPathSet(); + testIssue(); + testXChainBridge(); + testNumber(); + testObject(); + testArray(); + testEdgeCases(); + } +}; + +BEAST_DEFINE_TESTSUITE(STParsedJSON, protocol, ripple); + +} // namespace ripple diff --git a/src/test/rpc/AccountSet_test.cpp b/src/test/rpc/AccountSet_test.cpp index 3615a715cd..52dc331a2b 100644 --- a/src/test/rpc/AccountSet_test.cpp +++ b/src/test/rpc/AccountSet_test.cpp @@ -19,6 +19,8 @@ #include +#include + #include #include #include @@ -578,6 +580,32 @@ public: env.close(); } + void + testBadSigningKey() + { + using namespace test::jtx; + testcase("Bad signing key"); + Env env(*this); + Account const alice("alice"); + + env.fund(XRP(10000), alice); + env.close(); + + auto jtx = env.jt(noop("alice"), ter(temBAD_SIGNATURE)); + if (!BEAST_EXPECT(jtx.stx)) + return; + auto stx = std::make_shared(*jtx.stx); + stx->at(sfSigningPubKey) = makeSlice(std::string("badkey")); + + env.app().openLedger().modify([&](OpenView& view, beast::Journal j) { + auto const result = + ripple::apply(env.app(), view, *stx, tapNONE, j); + BEAST_EXPECT(result.ter == temBAD_SIGNATURE); + BEAST_EXPECT(!result.applied); + return result.applied; + }); + } + void run() override { @@ -594,6 +622,7 @@ public: testRequireAuthWithDir(); testTransferRate(); testTicket(); + testBadSigningKey(); } }; diff --git a/src/test/unit_test/FileDirGuard.h b/src/test/unit_test/FileDirGuard.h index 091bc80d20..d3cabc2092 100644 --- a/src/test/unit_test/FileDirGuard.h +++ b/src/test/unit_test/FileDirGuard.h @@ -29,7 +29,6 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. #include namespace ripple { -namespace test { namespace detail { /** @@ -178,7 +177,6 @@ public: }; } // namespace detail -} // namespace test } // namespace ripple #endif // TEST_UNIT_TEST_DIRGUARD_H diff --git a/src/test/unit_test/multi_runner.cpp b/src/test/unit_test/multi_runner.cpp index 087e37dac2..3755428ee3 100644 --- a/src/test/unit_test/multi_runner.cpp +++ b/src/test/unit_test/multi_runner.cpp @@ -30,7 +30,6 @@ #include namespace ripple { -namespace test { namespace detail { @@ -388,6 +387,8 @@ multi_runner_base::add_failures(std::size_t failures) } // namespace detail +namespace test { + //------------------------------------------------------------------------------ multi_runner_parent::multi_runner_parent() : os_(std::cout) @@ -645,10 +646,11 @@ multi_runner_child::on_log(std::string const& msg) message_queue_send(MessageType::log, s.str()); } +} // namespace test + namespace detail { template class multi_runner_base; template class multi_runner_base; } // namespace detail -} // namespace test } // namespace ripple diff --git a/src/test/unit_test/multi_runner.h b/src/test/unit_test/multi_runner.h index 08512d1882..bce62fb131 100644 --- a/src/test/unit_test/multi_runner.h +++ b/src/test/unit_test/multi_runner.h @@ -40,7 +40,6 @@ #include namespace ripple { -namespace test { namespace detail { @@ -212,6 +211,8 @@ public: } // namespace detail +namespace test { + //------------------------------------------------------------------------------ /** Manager for children running unit tests diff --git a/src/tests/libxrpl/CMakeLists.txt b/src/tests/libxrpl/CMakeLists.txt index 68c6fa6cb3..f97283c955 100644 --- a/src/tests/libxrpl/CMakeLists.txt +++ b/src/tests/libxrpl/CMakeLists.txt @@ -12,3 +12,5 @@ xrpl_add_test(basics) target_link_libraries(xrpl.test.basics PRIVATE xrpl.imports.test) xrpl_add_test(crypto) target_link_libraries(xrpl.test.crypto PRIVATE xrpl.imports.test) +xrpl_add_test(net) +target_link_libraries(xrpl.test.net PRIVATE xrpl.imports.test) diff --git a/src/tests/libxrpl/net/HTTPClient.cpp b/src/tests/libxrpl/net/HTTPClient.cpp new file mode 100644 index 0000000000..4d50c47220 --- /dev/null +++ b/src/tests/libxrpl/net/HTTPClient.cpp @@ -0,0 +1,346 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +using namespace ripple; + +namespace { + +// Simple HTTP server using Beast for testing +class TestHTTPServer +{ +private: + boost::asio::io_context ioc_; + boost::asio::ip::tcp::acceptor acceptor_; + boost::asio::ip::tcp::endpoint endpoint_; + std::atomic running_{true}; + unsigned short port_; + + // Custom headers to return + std::map custom_headers_; + std::string response_body_; + unsigned int status_code_{200}; + +public: + TestHTTPServer() : acceptor_(ioc_), port_(0) + { + // Bind to any available port + endpoint_ = {boost::asio::ip::tcp::v4(), 0}; + acceptor_.open(endpoint_.protocol()); + acceptor_.set_option(boost::asio::socket_base::reuse_address(true)); + acceptor_.bind(endpoint_); + acceptor_.listen(); + + // Get the actual port that was assigned + port_ = acceptor_.local_endpoint().port(); + + accept(); + } + + ~TestHTTPServer() + { + stop(); + } + + boost::asio::io_context& + ioc() + { + return ioc_; + } + + unsigned short + port() const + { + return port_; + } + + void + setHeader(std::string const& name, std::string const& value) + { + custom_headers_[name] = value; + } + + void + setResponseBody(std::string const& body) + { + response_body_ = body; + } + + void + setStatusCode(unsigned int code) + { + status_code_ = code; + } + +private: + void + stop() + { + running_ = false; + acceptor_.close(); + } + + void + accept() + { + if (!running_) + return; + + acceptor_.async_accept( + ioc_, + endpoint_, + [&](boost::system::error_code const& error, + boost::asio::ip::tcp::socket peer) { + if (!running_) + return; + + if (!error) + { + handleConnection(std::move(peer)); + } + }); + } + + void + handleConnection(boost::asio::ip::tcp::socket socket) + { + try + { + // Read the HTTP request + boost::beast::flat_buffer buffer; + boost::beast::http::request req; + boost::beast::http::read(socket, buffer, req); + + // Create response + boost::beast::http::response res; + res.version(req.version()); + res.result(status_code_); + res.set(boost::beast::http::field::server, "TestServer"); + + // Add custom headers + for (auto const& [name, value] : custom_headers_) + { + res.set(name, value); + } + + // Set body and prepare payload first + res.body() = response_body_; + res.prepare_payload(); + + // Override Content-Length with custom headers after prepare_payload + // This allows us to test case-insensitive header parsing + for (auto const& [name, value] : custom_headers_) + { + if (boost::iequals(name, "Content-Length")) + { + res.erase(boost::beast::http::field::content_length); + res.set(name, value); + } + } + + // Send response + boost::beast::http::write(socket, res); + + // Shutdown socket gracefully + boost::system::error_code ec; + socket.shutdown(boost::asio::ip::tcp::socket::shutdown_send, ec); + } + catch (std::exception const&) + { + // Connection handling errors are expected + } + + if (running_) + accept(); + } +}; + +// Helper function to run HTTP client test +bool +runHTTPTest( + TestHTTPServer& server, + std::string const& path, + std::atomic& completed, + std::atomic& result_status, + std::string& result_data, + boost::system::error_code& result_error) +{ + // Create a null journal for testing + beast::Journal j{beast::Journal::getNullSink()}; + + // Initialize HTTPClient SSL context + HTTPClient::initializeSSLContext("", "", false, j); + + HTTPClient::get( + false, // no SSL + server.ioc(), + "127.0.0.1", + server.port(), + path, + 1024, // max response size + std::chrono::seconds(5), + [&](boost::system::error_code const& ec, + int status, + std::string const& data) -> bool { + result_error = ec; + result_status = status; + result_data = data; + completed = true; + return false; // don't retry + }, + j); + + // Run the IO context until completion + auto start = std::chrono::steady_clock::now(); + while (!completed && + std::chrono::steady_clock::now() - start < std::chrono::seconds(10)) + { + if (server.ioc().run_one() == 0) + { + break; + } + } + + return completed; +} + +} // anonymous namespace + +TEST_CASE("HTTPClient case insensitive Content-Length") +{ + // Test different cases of Content-Length header + std::vector header_cases = { + "Content-Length", // Standard case + "content-length", // Lowercase - this tests the regex icase fix + "CONTENT-LENGTH", // Uppercase + "Content-length", // Mixed case + "content-Length" // Mixed case 2 + }; + + for (auto const& header_name : header_cases) + { + TestHTTPServer server; + std::string test_body = "Hello World!"; + server.setResponseBody(test_body); + server.setHeader(header_name, std::to_string(test_body.size())); + + std::atomic completed{false}; + std::atomic result_status{0}; + std::string result_data; + boost::system::error_code result_error; + + bool test_completed = runHTTPTest( + server, + "/test", + completed, + result_status, + result_data, + result_error); + + // Verify results + CHECK(test_completed); + CHECK(!result_error); + CHECK(result_status == 200); + CHECK(result_data == test_body); + } +} + +TEST_CASE("HTTPClient basic HTTP request") +{ + TestHTTPServer server; + std::string test_body = "Test response body"; + server.setResponseBody(test_body); + server.setHeader("Content-Type", "text/plain"); + + std::atomic completed{false}; + std::atomic result_status{0}; + std::string result_data; + boost::system::error_code result_error; + + bool test_completed = runHTTPTest( + server, "/basic", completed, result_status, result_data, result_error); + + CHECK(test_completed); + CHECK(!result_error); + CHECK(result_status == 200); + CHECK(result_data == test_body); +} + +TEST_CASE("HTTPClient empty response") +{ + TestHTTPServer server; + server.setResponseBody(""); // Empty body + server.setHeader("Content-Length", "0"); + + std::atomic completed{false}; + std::atomic result_status{0}; + std::string result_data; + boost::system::error_code result_error; + + bool test_completed = runHTTPTest( + server, "/empty", completed, result_status, result_data, result_error); + + CHECK(test_completed); + CHECK(!result_error); + CHECK(result_status == 200); + CHECK(result_data.empty()); +} + +TEST_CASE("HTTPClient different status codes") +{ + std::vector status_codes = {200, 404, 500}; + + for (auto status : status_codes) + { + TestHTTPServer server; + server.setStatusCode(status); + server.setResponseBody("Status " + std::to_string(status)); + + std::atomic completed{false}; + std::atomic result_status{0}; + std::string result_data; + boost::system::error_code result_error; + + bool test_completed = runHTTPTest( + server, + "/status", + completed, + result_status, + result_data, + result_error); + + CHECK(test_completed); + CHECK(!result_error); + CHECK(result_status == static_cast(status)); + } +} diff --git a/src/test/protocol/types_test.cpp b/src/tests/libxrpl/net/main.cpp similarity index 56% rename from src/test/protocol/types_test.cpp rename to src/tests/libxrpl/net/main.cpp index 8257d9c649..be9fc14bbf 100644 --- a/src/test/protocol/types_test.cpp +++ b/src/tests/libxrpl/net/main.cpp @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ /* This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2012, 2013 Ripple Labs Inc. + Copyright (c) 2024 Ripple Labs Inc. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above @@ -17,36 +17,5 @@ */ //============================================================================== -#include -#include - -namespace ripple { - -struct types_test : public beast::unit_test::suite -{ - void - testAccountID() - { - auto const s = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; - if (auto const parsed = parseBase58(s); BEAST_EXPECT(parsed)) - { - BEAST_EXPECT(toBase58(*parsed) == s); - } - - { - auto const s = - "âabcd1rNxp4h8apvRis6mJf9Sh8C6iRxfrDWNâabcdAVâ\xc2\x80\xc2\x8f"; - BEAST_EXPECT(!parseBase58(s)); - } - } - - void - run() override - { - testAccountID(); - } -}; - -BEAST_DEFINE_TESTSUITE(types, protocol, ripple); - -} // namespace ripple +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include diff --git a/src/xrpld/app/consensus/RCLCxLedger.h b/src/xrpld/app/consensus/RCLCxLedger.h index cd14c30a94..f9df1fe41a 100644 --- a/src/xrpld/app/consensus/RCLCxLedger.h +++ b/src/xrpld/app/consensus/RCLCxLedger.h @@ -22,8 +22,8 @@ #include #include -#include +#include #include namespace ripple { diff --git a/src/xrpld/app/ledger/BuildLedger.h b/src/xrpld/app/ledger/BuildLedger.h index 2ec571773c..980fe82ed8 100644 --- a/src/xrpld/app/ledger/BuildLedger.h +++ b/src/xrpld/app/ledger/BuildLedger.h @@ -20,10 +20,9 @@ #ifndef RIPPLE_APP_LEDGER_BUILD_LEDGER_H_INCLUDED #define RIPPLE_APP_LEDGER_BUILD_LEDGER_H_INCLUDED -#include - #include #include +#include namespace ripple { diff --git a/src/xrpld/app/ledger/Ledger.h b/src/xrpld/app/ledger/Ledger.h index 81c26526e5..552f59ff19 100644 --- a/src/xrpld/app/ledger/Ledger.h +++ b/src/xrpld/app/ledger/Ledger.h @@ -22,12 +22,12 @@ #include #include -#include -#include #include #include #include +#include +#include #include #include #include diff --git a/src/xrpld/app/ledger/LocalTxs.h b/src/xrpld/app/ledger/LocalTxs.h index 391bb4f7ef..2d202a5b60 100644 --- a/src/xrpld/app/ledger/LocalTxs.h +++ b/src/xrpld/app/ledger/LocalTxs.h @@ -21,7 +21,8 @@ #define RIPPLE_APP_LEDGER_LOCALTXS_H_INCLUDED #include -#include + +#include #include diff --git a/src/xrpld/app/ledger/OpenLedger.h b/src/xrpld/app/ledger/OpenLedger.h index 9fe56ff488..9383a53575 100644 --- a/src/xrpld/app/ledger/OpenLedger.h +++ b/src/xrpld/app/ledger/OpenLedger.h @@ -23,13 +23,13 @@ #include #include #include -#include -#include #include #include #include #include +#include +#include #include diff --git a/src/xrpld/app/ledger/detail/OpenLedger.cpp b/src/xrpld/app/ledger/detail/OpenLedger.cpp index 2c98caaa6d..5bba544e31 100644 --- a/src/xrpld/app/ledger/detail/OpenLedger.cpp +++ b/src/xrpld/app/ledger/detail/OpenLedger.cpp @@ -22,10 +22,10 @@ #include #include #include -#include #include #include +#include #include #include diff --git a/src/xrpld/app/main/Application.cpp b/src/xrpld/app/main/Application.cpp index beaf85ce2e..05b8f5e5fa 100644 --- a/src/xrpld/app/main/Application.cpp +++ b/src/xrpld/app/main/Application.cpp @@ -304,8 +304,8 @@ public: static_cast(std::thread::hardware_concurrency()); // Be more aggressive about the number of threads to use - // for the job queue if the server is configured as "large" - // or "huge" if there are enough cores. + // for the job queue if the server is configured as + // "large" or "huge" if there are enough cores. if (config->NODE_SIZE >= 4 && count >= 16) count = 6 + std::min(count, 8); else if (config->NODE_SIZE >= 3 && count >= 8) diff --git a/src/xrpld/app/misc/AMMUtils.h b/src/xrpld/app/misc/AMMUtils.h index 2a9f82ae60..d89085b116 100644 --- a/src/xrpld/app/misc/AMMUtils.h +++ b/src/xrpld/app/misc/AMMUtils.h @@ -20,10 +20,9 @@ #ifndef RIPPLE_APP_MISC_AMMUTILS_H_INCLUDED #define RIPPLE_APP_MISC_AMMUTILS_H_INCLUDED -#include - #include #include +#include #include #include #include diff --git a/src/xrpld/app/misc/FeeVote.h b/src/xrpld/app/misc/FeeVote.h index 35f723aa02..543456785a 100644 --- a/src/xrpld/app/misc/FeeVote.h +++ b/src/xrpld/app/misc/FeeVote.h @@ -20,9 +20,9 @@ #ifndef RIPPLE_APP_MISC_FEEVOTE_H_INCLUDED #define RIPPLE_APP_MISC_FEEVOTE_H_INCLUDED -#include #include +#include #include namespace ripple { diff --git a/src/xrpld/app/misc/NetworkOPs.cpp b/src/xrpld/app/misc/NetworkOPs.cpp index 403090c390..b9069442f8 100644 --- a/src/xrpld/app/misc/NetworkOPs.cpp +++ b/src/xrpld/app/misc/NetworkOPs.cpp @@ -1452,6 +1452,11 @@ NetworkOPsImp::processTransactionSet(CanonicalTXSet const& set) for (auto& t : transactions) mTransactions.push_back(std::move(t)); } + if (mTransactions.empty()) + { + JLOG(m_journal.debug()) << "No transaction to process!"; + return; + } doTransactionSyncBatch(lock, [&](std::unique_lock const&) { XRPL_ASSERT( diff --git a/src/xrpld/app/misc/NetworkOPs.h b/src/xrpld/app/misc/NetworkOPs.h index 9587d63b3a..bec0ad2341 100644 --- a/src/xrpld/app/misc/NetworkOPs.h +++ b/src/xrpld/app/misc/NetworkOPs.h @@ -23,9 +23,9 @@ #include #include #include -#include #include +#include #include #include diff --git a/src/xrpld/app/misc/PermissionedDEXHelpers.cpp b/src/xrpld/app/misc/PermissionedDEXHelpers.cpp index 4251ac1519..279ef8d7e9 100644 --- a/src/xrpld/app/misc/PermissionedDEXHelpers.cpp +++ b/src/xrpld/app/misc/PermissionedDEXHelpers.cpp @@ -17,9 +17,10 @@ */ //============================================================================== -#include #include +#include + namespace ripple { namespace permissioned_dex { diff --git a/src/xrpld/app/misc/PermissionedDEXHelpers.h b/src/xrpld/app/misc/PermissionedDEXHelpers.h index 1b3a0323fd..409b32f4ba 100644 --- a/src/xrpld/app/misc/PermissionedDEXHelpers.h +++ b/src/xrpld/app/misc/PermissionedDEXHelpers.h @@ -18,7 +18,7 @@ //============================================================================== #pragma once -#include +#include namespace ripple { namespace permissioned_dex { diff --git a/src/xrpld/app/misc/TxQ.h b/src/xrpld/app/misc/TxQ.h index f6ac2c6861..6afd83b0d0 100644 --- a/src/xrpld/app/misc/TxQ.h +++ b/src/xrpld/app/misc/TxQ.h @@ -21,9 +21,9 @@ #define RIPPLE_TXQ_H_INCLUDED #include -#include -#include +#include +#include #include #include #include diff --git a/src/xrpld/app/misc/ValidatorList.h b/src/xrpld/app/misc/ValidatorList.h index 1f5d728824..9a2018cbd4 100644 --- a/src/xrpld/app/misc/ValidatorList.h +++ b/src/xrpld/app/misc/ValidatorList.h @@ -877,7 +877,7 @@ private: verify( lock_guard const&, Json::Value& list, - std::string const& manifest, + Manifest manifest, std::string const& blob, std::string const& signature); diff --git a/src/xrpld/app/misc/detail/AMMUtils.cpp b/src/xrpld/app/misc/detail/AMMUtils.cpp index b56ce2748e..94cdb04287 100644 --- a/src/xrpld/app/misc/detail/AMMUtils.cpp +++ b/src/xrpld/app/misc/detail/AMMUtils.cpp @@ -19,9 +19,9 @@ #include #include -#include #include +#include #include #include diff --git a/src/xrpld/app/misc/detail/LoadFeeTrack.cpp b/src/xrpld/app/misc/detail/LoadFeeTrack.cpp index 96e7555401..4728df9272 100644 --- a/src/xrpld/app/misc/detail/LoadFeeTrack.cpp +++ b/src/xrpld/app/misc/detail/LoadFeeTrack.cpp @@ -18,12 +18,11 @@ //============================================================================== #include -#include #include #include #include -#include +#include #include diff --git a/src/xrpld/app/misc/detail/ValidatorList.cpp b/src/xrpld/app/misc/detail/ValidatorList.cpp index 1ddb51c9dd..2b45cec3be 100644 --- a/src/xrpld/app/misc/detail/ValidatorList.cpp +++ b/src/xrpld/app/misc/detail/ValidatorList.cpp @@ -1149,21 +1149,33 @@ ValidatorList::applyList( Json::Value list; auto const& manifest = localManifest ? *localManifest : globalManifest; - auto [result, pubKeyOpt] = verify(lock, list, manifest, blob, signature); + auto m = deserializeManifest(base64_decode(manifest)); + if (!m) + { + JLOG(j_.warn()) << "UNL manifest cannot be deserialized"; + return PublisherListStats{ListDisposition::invalid}; + } + + auto [result, pubKeyOpt] = + verify(lock, list, std::move(*m), blob, signature); if (!pubKeyOpt) { - JLOG(j_.info()) << "ValidatorList::applyList unable to retrieve the " - "master public key from the verify function\n"; + JLOG(j_.warn()) + << "UNL manifest is signed with an unrecognized master public key"; return PublisherListStats{result}; } if (!publicKeyType(*pubKeyOpt)) - { - JLOG(j_.info()) << "ValidatorList::applyList Invalid Public Key type" - " retrieved from the verify function\n "; + { // LCOV_EXCL_START + // This is an impossible situation because we will never load an + // invalid public key type (see checks in `ValidatorList::load`) however + // we can only arrive here if the key used by the manifest matched one of + // the loaded keys + UNREACHABLE( + "ripple::ValidatorList::applyList : invalid public key type"); return PublisherListStats{result}; - } + } // LCOV_EXCL_STOP PublicKey pubKey = *pubKeyOpt; if (result > ListDisposition::pending) @@ -1356,19 +1368,17 @@ std::pair> ValidatorList::verify( ValidatorList::lock_guard const& lock, Json::Value& list, - std::string const& manifest, + Manifest manifest, std::string const& blob, std::string const& signature) { - auto m = deserializeManifest(base64_decode(manifest)); - - if (!m || !publisherLists_.count(m->masterKey)) + if (!publisherLists_.count(manifest.masterKey)) return {ListDisposition::untrusted, {}}; - PublicKey masterPubKey = m->masterKey; - auto const revoked = m->revoked(); + PublicKey masterPubKey = manifest.masterKey; + auto const revoked = manifest.revoked(); - auto const result = publisherManifests_.applyManifest(std::move(*m)); + auto const result = publisherManifests_.applyManifest(std::move(manifest)); if (revoked && result == ManifestDisposition::accepted) { @@ -1796,7 +1806,7 @@ ValidatorList::getAvailable( if (!keyBlob || !publicKeyType(makeSlice(*keyBlob))) { - JLOG(j_.info()) << "Invalid requested validator list publisher key: " + JLOG(j_.warn()) << "Invalid requested validator list publisher key: " << pubKey; return {}; } diff --git a/src/xrpld/app/paths/AMMLiquidity.h b/src/xrpld/app/paths/AMMLiquidity.h index ee745b4a8a..cb1db93705 100644 --- a/src/xrpld/app/paths/AMMLiquidity.h +++ b/src/xrpld/app/paths/AMMLiquidity.h @@ -23,10 +23,10 @@ #include #include #include -#include -#include #include +#include +#include #include namespace ripple { diff --git a/src/xrpld/app/paths/AMMOffer.h b/src/xrpld/app/paths/AMMOffer.h index 9241ba2057..3c218c0f5e 100644 --- a/src/xrpld/app/paths/AMMOffer.h +++ b/src/xrpld/app/paths/AMMOffer.h @@ -20,9 +20,8 @@ #ifndef RIPPLE_APP_AMMOFFER_H_INCLUDED #define RIPPLE_APP_AMMOFFER_H_INCLUDED -#include -#include - +#include +#include #include #include diff --git a/src/xrpld/app/paths/Credit.cpp b/src/xrpld/app/paths/Credit.cpp index ca721e4edf..f975385b28 100644 --- a/src/xrpld/app/paths/Credit.cpp +++ b/src/xrpld/app/paths/Credit.cpp @@ -17,8 +17,7 @@ */ //============================================================================== -#include - +#include #include #include #include diff --git a/src/xrpld/app/paths/Credit.h b/src/xrpld/app/paths/Credit.h index 0ea2d687dd..12ea27a734 100644 --- a/src/xrpld/app/paths/Credit.h +++ b/src/xrpld/app/paths/Credit.h @@ -20,8 +20,7 @@ #ifndef RIPPLE_APP_PATHS_CREDIT_H_INCLUDED #define RIPPLE_APP_PATHS_CREDIT_H_INCLUDED -#include - +#include #include #include diff --git a/src/xrpld/app/paths/Pathfinder.cpp b/src/xrpld/app/paths/Pathfinder.cpp index 74a33ec917..41a3697888 100644 --- a/src/xrpld/app/paths/Pathfinder.cpp +++ b/src/xrpld/app/paths/Pathfinder.cpp @@ -24,11 +24,11 @@ #include #include #include -#include #include #include #include +#include #include diff --git a/src/xrpld/app/paths/RippleCalc.cpp b/src/xrpld/app/paths/RippleCalc.cpp index 9c438bdfa9..237f0b8f9c 100644 --- a/src/xrpld/app/paths/RippleCalc.cpp +++ b/src/xrpld/app/paths/RippleCalc.cpp @@ -20,9 +20,9 @@ #include #include #include -#include #include +#include #include namespace ripple { diff --git a/src/xrpld/app/paths/RippleCalc.h b/src/xrpld/app/paths/RippleCalc.h index 09de7334e8..527c26f2ca 100644 --- a/src/xrpld/app/paths/RippleCalc.h +++ b/src/xrpld/app/paths/RippleCalc.h @@ -20,9 +20,8 @@ #ifndef RIPPLE_APP_PATHS_RIPPLECALC_H_INCLUDED #define RIPPLE_APP_PATHS_RIPPLECALC_H_INCLUDED -#include - #include +#include #include #include diff --git a/src/xrpld/app/paths/TrustLine.h b/src/xrpld/app/paths/TrustLine.h index efbe281f5e..5cff89b177 100644 --- a/src/xrpld/app/paths/TrustLine.h +++ b/src/xrpld/app/paths/TrustLine.h @@ -20,9 +20,8 @@ #ifndef RIPPLE_APP_PATHS_RIPPLESTATE_H_INCLUDED #define RIPPLE_APP_PATHS_RIPPLESTATE_H_INCLUDED -#include - #include +#include #include #include #include diff --git a/src/xrpld/app/paths/detail/BookStep.cpp b/src/xrpld/app/paths/detail/BookStep.cpp index 554d2525f5..97cf87c046 100644 --- a/src/xrpld/app/paths/detail/BookStep.cpp +++ b/src/xrpld/app/paths/detail/BookStep.cpp @@ -23,11 +23,11 @@ #include #include #include -#include #include #include #include +#include #include #include #include diff --git a/src/xrpld/app/paths/detail/DirectStep.cpp b/src/xrpld/app/paths/detail/DirectStep.cpp index 5e62a289a3..03d207e008 100644 --- a/src/xrpld/app/paths/detail/DirectStep.cpp +++ b/src/xrpld/app/paths/detail/DirectStep.cpp @@ -20,9 +20,9 @@ #include #include #include -#include #include +#include #include #include #include diff --git a/src/xrpld/app/paths/detail/FlowDebugInfo.h b/src/xrpld/app/paths/detail/FlowDebugInfo.h index e28b34f5d1..eec1d7c5a6 100644 --- a/src/xrpld/app/paths/detail/FlowDebugInfo.h +++ b/src/xrpld/app/paths/detail/FlowDebugInfo.h @@ -21,8 +21,8 @@ #define RIPPLE_PATH_IMPL_FLOWDEBUGINFO_H_INCLUDED #include -#include +#include #include #include diff --git a/src/xrpld/app/paths/detail/PaySteps.cpp b/src/xrpld/app/paths/detail/PaySteps.cpp index aa9e21e182..6eb38eee83 100644 --- a/src/xrpld/app/paths/detail/PaySteps.cpp +++ b/src/xrpld/app/paths/detail/PaySteps.cpp @@ -18,10 +18,10 @@ //============================================================================== #include -#include #include #include +#include #include #include diff --git a/src/xrpld/app/paths/detail/StepChecks.h b/src/xrpld/app/paths/detail/StepChecks.h index 4acafd1b9a..5ca2a463cb 100644 --- a/src/xrpld/app/paths/detail/StepChecks.h +++ b/src/xrpld/app/paths/detail/StepChecks.h @@ -20,11 +20,10 @@ #ifndef RIPPLE_APP_PATHS_IMPL_STEP_CHECKS_H_INCLUDED #define RIPPLE_APP_PATHS_IMPL_STEP_CHECKS_H_INCLUDED -#include -#include - #include #include +#include +#include #include #include diff --git a/src/xrpld/app/paths/detail/XRPEndpointStep.cpp b/src/xrpld/app/paths/detail/XRPEndpointStep.cpp index 7fdfb3749d..9cbcb0c84d 100644 --- a/src/xrpld/app/paths/detail/XRPEndpointStep.cpp +++ b/src/xrpld/app/paths/detail/XRPEndpointStep.cpp @@ -21,9 +21,9 @@ #include #include #include -#include #include +#include #include #include #include diff --git a/src/xrpld/app/tx/apply.h b/src/xrpld/app/tx/apply.h index 101f9a946d..7f43d1a744 100644 --- a/src/xrpld/app/tx/apply.h +++ b/src/xrpld/app/tx/apply.h @@ -22,9 +22,9 @@ #include #include -#include #include +#include #include #include diff --git a/src/xrpld/app/tx/applySteps.h b/src/xrpld/app/tx/applySteps.h index ec7180e263..a543ac37de 100644 --- a/src/xrpld/app/tx/applySteps.h +++ b/src/xrpld/app/tx/applySteps.h @@ -20,9 +20,8 @@ #ifndef RIPPLE_TX_APPLYSTEPS_H_INCLUDED #define RIPPLE_TX_APPLYSTEPS_H_INCLUDED -#include - #include +#include namespace ripple { diff --git a/src/xrpld/app/tx/detail/AMMBid.cpp b/src/xrpld/app/tx/detail/AMMBid.cpp index 86a80431b4..769668b07b 100644 --- a/src/xrpld/app/tx/detail/AMMBid.cpp +++ b/src/xrpld/app/tx/detail/AMMBid.cpp @@ -20,9 +20,9 @@ #include #include #include -#include -#include +#include +#include #include #include #include @@ -30,21 +30,15 @@ namespace ripple { +bool +AMMBid::checkExtraFeatures(PreflightContext const& ctx) +{ + return ammEnabled(ctx.rules); +} + NotTEC AMMBid::preflight(PreflightContext const& ctx) { - if (!ammEnabled(ctx.rules)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfUniversalMask) - { - JLOG(ctx.j.debug()) << "AMM Bid: invalid flags."; - return temINVALID_FLAG; - } - if (auto const res = invalidAMMAssetPair( ctx.tx[sfAsset].get(), ctx.tx[sfAsset2].get())) { @@ -95,7 +89,7 @@ AMMBid::preflight(PreflightContext const& ctx) } } - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/AMMBid.h b/src/xrpld/app/tx/detail/AMMBid.h index 4bb3a2adfd..4a527b6a93 100644 --- a/src/xrpld/app/tx/detail/AMMBid.h +++ b/src/xrpld/app/tx/detail/AMMBid.h @@ -71,6 +71,9 @@ public: { } + static bool + checkExtraFeatures(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/AMMClawback.cpp b/src/xrpld/app/tx/detail/AMMClawback.cpp index 07c5151727..9a79c94a58 100644 --- a/src/xrpld/app/tx/detail/AMMClawback.cpp +++ b/src/xrpld/app/tx/detail/AMMClawback.cpp @@ -21,9 +21,9 @@ #include #include #include -#include -#include +#include +#include #include #include #include @@ -33,19 +33,15 @@ namespace ripple { +std::uint32_t +AMMClawback::getFlagsMask(PreflightContext const& ctx) +{ + return tfAMMClawbackMask; +} + NotTEC AMMClawback::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureAMMClawback)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; // LCOV_EXCL_LINE - - auto const flags = ctx.tx.getFlags(); - if (flags & tfAMMClawbackMask) - return temINVALID_FLAG; - AccountID const issuer = ctx.tx[sfAccount]; AccountID const holder = ctx.tx[sfHolder]; @@ -63,6 +59,8 @@ AMMClawback::preflight(PreflightContext const& ctx) if (isXRP(asset)) return temMALFORMED; + auto const flags = ctx.tx.getFlags(); + if (flags & tfClawTwoAssets && asset.account != asset2.account) { JLOG(ctx.j.trace()) @@ -88,7 +86,7 @@ AMMClawback::preflight(PreflightContext const& ctx) if (clawAmount && *clawAmount <= beast::zero) return temBAD_AMOUNT; - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/AMMClawback.h b/src/xrpld/app/tx/detail/AMMClawback.h index fdcfc53e2c..1984937971 100644 --- a/src/xrpld/app/tx/detail/AMMClawback.h +++ b/src/xrpld/app/tx/detail/AMMClawback.h @@ -33,6 +33,9 @@ public: { } + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/AMMCreate.cpp b/src/xrpld/app/tx/detail/AMMCreate.cpp index f0ccc6f298..63e20b42fb 100644 --- a/src/xrpld/app/tx/detail/AMMCreate.cpp +++ b/src/xrpld/app/tx/detail/AMMCreate.cpp @@ -21,9 +21,9 @@ #include #include #include -#include -#include +#include +#include #include #include #include @@ -31,21 +31,15 @@ namespace ripple { +bool +AMMCreate::checkExtraFeatures(PreflightContext const& ctx) +{ + return ammEnabled(ctx.rules); +} + NotTEC AMMCreate::preflight(PreflightContext const& ctx) { - if (!ammEnabled(ctx.rules)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfUniversalMask) - { - JLOG(ctx.j.debug()) << "AMM Instance: invalid flags."; - return temINVALID_FLAG; - } - auto const amount = ctx.tx[sfAmount]; auto const amount2 = ctx.tx[sfAmount2]; @@ -74,14 +68,14 @@ AMMCreate::preflight(PreflightContext const& ctx) return temBAD_FEE; } - return preflight2(ctx); + return tesSUCCESS; } XRPAmount AMMCreate::calculateBaseFee(ReadView const& view, STTx const& tx) { // The fee required for AMMCreate is one owner reserve. - return view.fees().increment; + return calculateOwnerReserveFee(view, tx); } TER diff --git a/src/xrpld/app/tx/detail/AMMCreate.h b/src/xrpld/app/tx/detail/AMMCreate.h index 189d66a55a..98231e5554 100644 --- a/src/xrpld/app/tx/detail/AMMCreate.h +++ b/src/xrpld/app/tx/detail/AMMCreate.h @@ -63,6 +63,9 @@ public: { } + static bool + checkExtraFeatures(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/AMMDelete.cpp b/src/xrpld/app/tx/detail/AMMDelete.cpp index 28d56eab98..663a4c4b0a 100644 --- a/src/xrpld/app/tx/detail/AMMDelete.cpp +++ b/src/xrpld/app/tx/detail/AMMDelete.cpp @@ -19,30 +19,24 @@ #include #include -#include +#include #include #include #include namespace ripple { +bool +AMMDelete::checkExtraFeatures(PreflightContext const& ctx) +{ + return ammEnabled(ctx.rules); +} + NotTEC AMMDelete::preflight(PreflightContext const& ctx) { - if (!ammEnabled(ctx.rules)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfUniversalMask) - { - JLOG(ctx.j.debug()) << "AMM Delete: invalid flags."; - return temINVALID_FLAG; - } - - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/AMMDelete.h b/src/xrpld/app/tx/detail/AMMDelete.h index 19885b1dad..36dace2e18 100644 --- a/src/xrpld/app/tx/detail/AMMDelete.h +++ b/src/xrpld/app/tx/detail/AMMDelete.h @@ -39,6 +39,9 @@ public: { } + static bool + checkExtraFeatures(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/AMMDeposit.cpp b/src/xrpld/app/tx/detail/AMMDeposit.cpp index 0dafa0da6c..8a3e50ed63 100644 --- a/src/xrpld/app/tx/detail/AMMDeposit.cpp +++ b/src/xrpld/app/tx/detail/AMMDeposit.cpp @@ -20,30 +20,32 @@ #include #include #include -#include -#include +#include +#include #include #include #include namespace ripple { +bool +AMMDeposit::checkExtraFeatures(PreflightContext const& ctx) +{ + return ammEnabled(ctx.rules); +} + +std::uint32_t +AMMDeposit::getFlagsMask(PreflightContext const& ctx) + +{ + return tfDepositMask; +} + NotTEC AMMDeposit::preflight(PreflightContext const& ctx) { - if (!ammEnabled(ctx.rules)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - auto const flags = ctx.tx.getFlags(); - if (flags & tfDepositMask) - { - JLOG(ctx.j.debug()) << "AMM Deposit: invalid flags."; - return temINVALID_FLAG; - } auto const amount = ctx.tx[~sfAmount]; auto const amount2 = ctx.tx[~sfAmount2]; @@ -159,7 +161,7 @@ AMMDeposit::preflight(PreflightContext const& ctx) return temBAD_FEE; } - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/AMMDeposit.h b/src/xrpld/app/tx/detail/AMMDeposit.h index 0acb1dd9ab..c1a37be452 100644 --- a/src/xrpld/app/tx/detail/AMMDeposit.h +++ b/src/xrpld/app/tx/detail/AMMDeposit.h @@ -68,6 +68,12 @@ public: { } + static bool + checkExtraFeatures(PreflightContext const& ctx); + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/AMMVote.cpp b/src/xrpld/app/tx/detail/AMMVote.cpp index 84d0905a22..0ffbb38b37 100644 --- a/src/xrpld/app/tx/detail/AMMVote.cpp +++ b/src/xrpld/app/tx/detail/AMMVote.cpp @@ -19,23 +19,23 @@ #include #include -#include +#include #include #include #include namespace ripple { +bool +AMMVote::checkExtraFeatures(PreflightContext const& ctx) +{ + return ammEnabled(ctx.rules); +} + NotTEC AMMVote::preflight(PreflightContext const& ctx) { - if (!ammEnabled(ctx.rules)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - if (auto const res = invalidAMMAssetPair( ctx.tx[sfAsset].get(), ctx.tx[sfAsset2].get())) { @@ -43,19 +43,13 @@ AMMVote::preflight(PreflightContext const& ctx) return res; } - if (ctx.tx.getFlags() & tfUniversalMask) - { - JLOG(ctx.j.debug()) << "AMM Vote: invalid flags."; - return temINVALID_FLAG; - } - if (ctx.tx[sfTradingFee] > TRADING_FEE_THRESHOLD) { JLOG(ctx.j.debug()) << "AMM Vote: invalid trading fee."; return temBAD_FEE; } - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/AMMVote.h b/src/xrpld/app/tx/detail/AMMVote.h index 2bee01aff5..dc99480111 100644 --- a/src/xrpld/app/tx/detail/AMMVote.h +++ b/src/xrpld/app/tx/detail/AMMVote.h @@ -56,6 +56,9 @@ public: { } + static bool + checkExtraFeatures(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/AMMWithdraw.cpp b/src/xrpld/app/tx/detail/AMMWithdraw.cpp index 2ad1a19df5..f5af9dfb9c 100644 --- a/src/xrpld/app/tx/detail/AMMWithdraw.cpp +++ b/src/xrpld/app/tx/detail/AMMWithdraw.cpp @@ -20,29 +20,30 @@ #include #include #include -#include #include +#include #include #include namespace ripple { +bool +AMMWithdraw::checkExtraFeatures(PreflightContext const& ctx) +{ + return ammEnabled(ctx.rules); +} + +std::uint32_t +AMMWithdraw::getFlagsMask(PreflightContext const& ctx) +{ + return tfWithdrawMask; +} + NotTEC AMMWithdraw::preflight(PreflightContext const& ctx) { - if (!ammEnabled(ctx.rules)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - auto const flags = ctx.tx.getFlags(); - if (flags & tfWithdrawMask) - { - JLOG(ctx.j.debug()) << "AMM Withdraw: invalid flags."; - return temINVALID_FLAG; - } auto const amount = ctx.tx[~sfAmount]; auto const amount2 = ctx.tx[~sfAmount2]; @@ -150,7 +151,7 @@ AMMWithdraw::preflight(PreflightContext const& ctx) } } - return preflight2(ctx); + return tesSUCCESS; } static std::optional diff --git a/src/xrpld/app/tx/detail/AMMWithdraw.h b/src/xrpld/app/tx/detail/AMMWithdraw.h index 1de91fd787..31a7904626 100644 --- a/src/xrpld/app/tx/detail/AMMWithdraw.h +++ b/src/xrpld/app/tx/detail/AMMWithdraw.h @@ -21,7 +21,8 @@ #define RIPPLE_TX_AMMWITHDRAW_H_INCLUDED #include -#include + +#include namespace ripple { @@ -75,6 +76,12 @@ public: { } + static bool + checkExtraFeatures(PreflightContext const& ctx); + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/ApplyContext.h b/src/xrpld/app/tx/detail/ApplyContext.h index 720d0aeea3..0344771a43 100644 --- a/src/xrpld/app/tx/detail/ApplyContext.h +++ b/src/xrpld/app/tx/detail/ApplyContext.h @@ -22,9 +22,9 @@ #include #include -#include #include +#include #include #include diff --git a/src/xrpld/app/tx/detail/Batch.cpp b/src/xrpld/app/tx/detail/Batch.cpp index 40991ea99a..cba89348d0 100644 --- a/src/xrpld/app/tx/detail/Batch.cpp +++ b/src/xrpld/app/tx/detail/Batch.cpp @@ -19,10 +19,10 @@ #include #include -#include -#include #include +#include +#include #include #include #include @@ -164,6 +164,12 @@ Batch::calculateBaseFee(ReadView const& view, STTx const& tx) return signerFees + txnFees + batchBase; } +std::uint32_t +Batch::getFlagsMask(PreflightContext const& ctx) +{ + return tfBatchMask; +} + /** * @brief Performs preflight validation checks for a Batch transaction. * @@ -200,23 +206,9 @@ Batch::calculateBaseFee(ReadView const& view, STTx const& tx) NotTEC Batch::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureBatch)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - auto const parentBatchId = ctx.tx.getTransactionID(); - auto const outerAccount = ctx.tx.getAccountID(sfAccount); auto const flags = ctx.tx.getFlags(); - if (flags & tfBatchMask) - { - JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]:" - << "invalid flags."; - return temINVALID_FLAG; - } - if (std::popcount( flags & (tfAllOrNothing | tfOnlyOne | tfUntilFailure | tfIndependent)) != 1) @@ -242,7 +234,6 @@ Batch::preflight(PreflightContext const& ctx) } // Validation Inner Batch Txns - std::unordered_set requiredSigners; std::unordered_set uniqueHashes; std::unordered_map> accountSeqTicket; @@ -372,6 +363,23 @@ Batch::preflight(PreflightContext const& ctx) } } } + } + + return tesSUCCESS; +} + +NotTEC +Batch::preflightSigValidated(PreflightContext const& ctx) +{ + auto const parentBatchId = ctx.tx.getTransactionID(); + auto const outerAccount = ctx.tx.getAccountID(sfAccount); + auto const& rawTxns = ctx.tx.getFieldArray(sfRawTransactions); + + // Build the signers list + std::unordered_set requiredSigners; + for (STObject const& rb : rawTxns) + { + auto const innerAccount = rb.getAccountID(sfAccount); // If the inner account is the same as the outer account, do not add the // inner account to the required signers set. @@ -379,11 +387,6 @@ Batch::preflight(PreflightContext const& ctx) requiredSigners.insert(innerAccount); } - // LCOV_EXCL_START - if (auto const ret = preflight2(ctx); !isTesSuccess(ret)) - return ret; - // LCOV_EXCL_STOP - // Validation Batch Signers std::unordered_set batchSigners; if (ctx.tx.isFieldPresent(sfBatchSigners)) diff --git a/src/xrpld/app/tx/detail/Batch.h b/src/xrpld/app/tx/detail/Batch.h index 211bce0589..07863a5f33 100644 --- a/src/xrpld/app/tx/detail/Batch.h +++ b/src/xrpld/app/tx/detail/Batch.h @@ -40,9 +40,15 @@ public: static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); + static NotTEC + preflightSigValidated(PreflightContext const& ctx); + static NotTEC checkSign(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/BookTip.h b/src/xrpld/app/tx/detail/BookTip.h index f215cdb620..0a9c49a4e9 100644 --- a/src/xrpld/app/tx/detail/BookTip.h +++ b/src/xrpld/app/tx/detail/BookTip.h @@ -20,8 +20,7 @@ #ifndef RIPPLE_APP_BOOK_BOOKTIP_H_INCLUDED #define RIPPLE_APP_BOOK_BOOKTIP_H_INCLUDED -#include - +#include #include #include diff --git a/src/xrpld/app/tx/detail/CancelCheck.cpp b/src/xrpld/app/tx/detail/CancelCheck.cpp index cfa3bd10e2..f1a9b42a89 100644 --- a/src/xrpld/app/tx/detail/CancelCheck.cpp +++ b/src/xrpld/app/tx/detail/CancelCheck.cpp @@ -19,9 +19,9 @@ #include #include -#include #include +#include #include #include #include @@ -32,21 +32,7 @@ namespace ripple { NotTEC CancelCheck::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureChecks)) - return temDISABLED; - - NotTEC const ret{preflight1(ctx)}; - if (!isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfUniversalMask) - { - // There are no flags (other than universal) for CreateCheck yet. - JLOG(ctx.j.warn()) << "Malformed transaction: Invalid flags set."; - return temINVALID_FLAG; - } - - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/CancelOffer.cpp b/src/xrpld/app/tx/detail/CancelOffer.cpp index 004ae1e8b9..e7ec28ce17 100644 --- a/src/xrpld/app/tx/detail/CancelOffer.cpp +++ b/src/xrpld/app/tx/detail/CancelOffer.cpp @@ -18,9 +18,9 @@ //============================================================================== #include -#include #include +#include #include namespace ripple { @@ -28,25 +28,13 @@ namespace ripple { NotTEC CancelOffer::preflight(PreflightContext const& ctx) { - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - auto const uTxFlags = ctx.tx.getFlags(); - - if (uTxFlags & tfUniversalMask) - { - JLOG(ctx.j.trace()) << "Malformed transaction: " - << "Invalid flags set."; - return temINVALID_FLAG; - } - if (!ctx.tx[sfOfferSequence]) { JLOG(ctx.j.trace()) << "CancelOffer::preflight: missing sequence"; return temBAD_SEQUENCE; } - return preflight2(ctx); + return tesSUCCESS; } //------------------------------------------------------------------------------ diff --git a/src/xrpld/app/tx/detail/CashCheck.cpp b/src/xrpld/app/tx/detail/CashCheck.cpp index 0f1d08689c..f8ab6189a3 100644 --- a/src/xrpld/app/tx/detail/CashCheck.cpp +++ b/src/xrpld/app/tx/detail/CashCheck.cpp @@ -35,20 +35,6 @@ namespace ripple { NotTEC CashCheck::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureChecks)) - return temDISABLED; - - NotTEC const ret{preflight1(ctx)}; - if (!isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfUniversalMask) - { - // There are no flags (other than universal) for CashCheck yet. - JLOG(ctx.j.warn()) << "Malformed transaction: Invalid flags set."; - return temINVALID_FLAG; - } - // Exactly one of Amount or DeliverMin must be present. auto const optAmount = ctx.tx[~sfAmount]; auto const optDeliverMin = ctx.tx[~sfDeliverMin]; @@ -76,7 +62,7 @@ CashCheck::preflight(PreflightContext const& ctx) return temBAD_CURRENCY; } - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/Change.cpp b/src/xrpld/app/tx/detail/Change.cpp index 1392d84c08..d6a31024f3 100644 --- a/src/xrpld/app/tx/detail/Change.cpp +++ b/src/xrpld/app/tx/detail/Change.cpp @@ -22,9 +22,9 @@ #include #include #include -#include #include +#include #include #include #include @@ -33,11 +33,12 @@ namespace ripple { +template <> NotTEC -Change::preflight(PreflightContext const& ctx) +Transactor::invokePreflight(PreflightContext const& ctx) { - auto const ret = preflight0(ctx); - if (!isTesSuccess(ret)) + // 0 means "Allow any flags" + if (auto const ret = preflight0(ctx, 0)) return ret; auto account = ctx.tx.getAccountID(sfAccount); diff --git a/src/xrpld/app/tx/detail/Change.h b/src/xrpld/app/tx/detail/Change.h index d710827dd6..7b8fbf3421 100644 --- a/src/xrpld/app/tx/detail/Change.h +++ b/src/xrpld/app/tx/detail/Change.h @@ -33,9 +33,6 @@ public: { } - static NotTEC - preflight(PreflightContext const& ctx); - TER doApply() override; void diff --git a/src/xrpld/app/tx/detail/Clawback.cpp b/src/xrpld/app/tx/detail/Clawback.cpp index 41ab1256fb..b346e4a1c1 100644 --- a/src/xrpld/app/tx/detail/Clawback.cpp +++ b/src/xrpld/app/tx/detail/Clawback.cpp @@ -18,8 +18,8 @@ //============================================================================== #include -#include +#include #include #include #include @@ -75,25 +75,22 @@ preflightHelper(PreflightContext const& ctx) return tesSUCCESS; } +std::uint32_t +Clawback::getFlagsMask(PreflightContext const& ctx) +{ + return tfClawbackMask; +} + NotTEC Clawback::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureClawback)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfClawbackMask) - return temINVALID_FLAG; - if (auto const ret = std::visit( [&](T const&) { return preflightHelper(ctx); }, ctx.tx[sfAmount].asset().value()); !isTesSuccess(ret)) return ret; - return preflight2(ctx); + return tesSUCCESS; } template diff --git a/src/xrpld/app/tx/detail/Clawback.h b/src/xrpld/app/tx/detail/Clawback.h index d908a2e4ef..b02233c2ed 100644 --- a/src/xrpld/app/tx/detail/Clawback.h +++ b/src/xrpld/app/tx/detail/Clawback.h @@ -33,6 +33,9 @@ public: { } + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/CreateCheck.cpp b/src/xrpld/app/tx/detail/CreateCheck.cpp index 9baceef944..57f3a92255 100644 --- a/src/xrpld/app/tx/detail/CreateCheck.cpp +++ b/src/xrpld/app/tx/detail/CreateCheck.cpp @@ -18,9 +18,9 @@ //============================================================================== #include -#include #include +#include #include #include #include @@ -31,19 +31,6 @@ namespace ripple { NotTEC CreateCheck::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureChecks)) - return temDISABLED; - - NotTEC const ret{preflight1(ctx)}; - if (!isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfUniversalMask) - { - // There are no flags (other than universal) for CreateCheck yet. - JLOG(ctx.j.warn()) << "Malformed transaction: Invalid flags set."; - return temINVALID_FLAG; - } if (ctx.tx[sfAccount] == ctx.tx[sfDestination]) { // They wrote a check to themselves. @@ -76,7 +63,7 @@ CreateCheck::preflight(PreflightContext const& ctx) } } - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/CreateOffer.cpp b/src/xrpld/app/tx/detail/CreateOffer.cpp index 3cfae92cbd..86750eb51d 100644 --- a/src/xrpld/app/tx/detail/CreateOffer.cpp +++ b/src/xrpld/app/tx/detail/CreateOffer.cpp @@ -21,10 +21,10 @@ #include #include #include -#include #include #include +#include #include #include #include @@ -43,30 +43,36 @@ CreateOffer::makeTxConsequences(PreflightContext const& ctx) return TxConsequences{ctx.tx, calculateMaxXRPSpend(ctx.tx)}; } -NotTEC -CreateOffer::preflight(PreflightContext const& ctx) +bool +CreateOffer::checkExtraFeatures(PreflightContext const& ctx) { if (ctx.tx.isFieldPresent(sfDomainID) && !ctx.rules.enabled(featurePermissionedDEX)) - return temDISABLED; + return false; - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; + return true; +} +std::uint32_t +CreateOffer::getFlagsMask(PreflightContext const& ctx) +{ + // The tfOfferCreateMask is built assuming that PermissionedDEX is + // enabled + if (ctx.rules.enabled(featurePermissionedDEX)) + return tfOfferCreateMask; + // If PermissionedDEX is not enabled, add tfHybrid to the mask, + // indicating it is not allowed. + return tfOfferCreateMask | tfHybrid; +} + +NotTEC +CreateOffer::preflight(PreflightContext const& ctx) +{ auto& tx = ctx.tx; auto& j = ctx.j; std::uint32_t const uTxFlags = tx.getFlags(); - if (uTxFlags & tfOfferCreateMask) - { - JLOG(j.debug()) << "Malformed transaction: Invalid flags set."; - return temINVALID_FLAG; - } - - if (!ctx.rules.enabled(featurePermissionedDEX) && tx.isFlag(tfHybrid)) - return temINVALID_FLAG; - if (tx.isFlag(tfHybrid) && !tx.isFieldPresent(sfDomainID)) return temINVALID_FLAG; @@ -136,7 +142,7 @@ CreateOffer::preflight(PreflightContext const& ctx) return temBAD_ISSUER; } - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/CreateOffer.h b/src/xrpld/app/tx/detail/CreateOffer.h index 6e3d6145b1..c38e244b34 100644 --- a/src/xrpld/app/tx/detail/CreateOffer.h +++ b/src/xrpld/app/tx/detail/CreateOffer.h @@ -43,6 +43,12 @@ public: static TxConsequences makeTxConsequences(PreflightContext const& ctx); + static bool + checkExtraFeatures(PreflightContext const& ctx); + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + /** Enforce constraints beyond those of the Transactor base class. */ static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/CreateTicket.cpp b/src/xrpld/app/tx/detail/CreateTicket.cpp index 594335f489..d48da2d780 100644 --- a/src/xrpld/app/tx/detail/CreateTicket.cpp +++ b/src/xrpld/app/tx/detail/CreateTicket.cpp @@ -36,20 +36,11 @@ CreateTicket::makeTxConsequences(PreflightContext const& ctx) NotTEC CreateTicket::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureTicketBatch)) - return temDISABLED; - - if (ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; - if (std::uint32_t const count = ctx.tx[sfTicketCount]; count < minValidCount || count > maxValidCount) return temINVALID_COUNT; - if (NotTEC const ret{preflight1(ctx)}; !isTesSuccess(ret)) - return ret; - - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/Credentials.cpp b/src/xrpld/app/tx/detail/Credentials.cpp index 73c397cf37..4b77163c5d 100644 --- a/src/xrpld/app/tx/detail/Credentials.cpp +++ b/src/xrpld/app/tx/detail/Credentials.cpp @@ -17,12 +17,12 @@ */ //============================================================================== -#include #include -#include -#include #include +#include +#include +#include #include #include #include @@ -48,28 +48,19 @@ using namespace credentials; // ------- CREATE -------------------------- +std::uint32_t +CredentialCreate::getFlagsMask(PreflightContext const& ctx) +{ + // 0 means "Allow any flags" + return ctx.rules.enabled(fixInvalidTxFlags) ? tfUniversalMask : 0; +} + NotTEC CredentialCreate::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureCredentials)) - { - JLOG(ctx.j.trace()) << "featureCredentials is disabled."; - return temDISABLED; - } - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - auto const& tx = ctx.tx; auto& j = ctx.j; - if (ctx.rules.enabled(fixInvalidTxFlags) && - (tx.getFlags() & tfUniversalMask)) - { - JLOG(ctx.j.debug()) << "CredentialCreate: invalid flags."; - return temINVALID_FLAG; - } - if (!tx[sfSubject]) { JLOG(j.trace()) << "Malformed transaction: Invalid Subject"; @@ -91,7 +82,7 @@ CredentialCreate::preflight(PreflightContext const& ctx) return temMALFORMED; } - return preflight2(ctx); + return tesSUCCESS; } TER @@ -202,25 +193,17 @@ CredentialCreate::doApply() } // ------- DELETE -------------------------- + +std::uint32_t +CredentialDelete::getFlagsMask(PreflightContext const& ctx) +{ + // 0 means "Allow any flags" + return ctx.rules.enabled(fixInvalidTxFlags) ? tfUniversalMask : 0; +} + NotTEC CredentialDelete::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureCredentials)) - { - JLOG(ctx.j.trace()) << "featureCredentials is disabled."; - return temDISABLED; - } - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.rules.enabled(fixInvalidTxFlags) && - (ctx.tx.getFlags() & tfUniversalMask)) - { - JLOG(ctx.j.debug()) << "CredentialDelete: invalid flags."; - return temINVALID_FLAG; - } - auto const subject = ctx.tx[~sfSubject]; auto const issuer = ctx.tx[~sfIssuer]; @@ -248,7 +231,7 @@ CredentialDelete::preflight(PreflightContext const& ctx) return temMALFORMED; } - return preflight2(ctx); + return tesSUCCESS; } TER @@ -289,25 +272,16 @@ CredentialDelete::doApply() // ------- APPLY -------------------------- +std::uint32_t +CredentialAccept::getFlagsMask(PreflightContext const& ctx) +{ + // 0 means "Allow any flags" + return ctx.rules.enabled(fixInvalidTxFlags) ? tfUniversalMask : 0; +} + NotTEC CredentialAccept::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureCredentials)) - { - JLOG(ctx.j.trace()) << "featureCredentials is disabled."; - return temDISABLED; - } - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.rules.enabled(fixInvalidTxFlags) && - (ctx.tx.getFlags() & tfUniversalMask)) - { - JLOG(ctx.j.debug()) << "CredentialAccept: invalid flags."; - return temINVALID_FLAG; - } - if (!ctx.tx[sfIssuer]) { JLOG(ctx.j.trace()) << "Malformed transaction: Issuer field zeroed."; @@ -322,7 +296,7 @@ CredentialAccept::preflight(PreflightContext const& ctx) return temMALFORMED; } - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/Credentials.h b/src/xrpld/app/tx/detail/Credentials.h index 5b4acb3998..a5885a2226 100644 --- a/src/xrpld/app/tx/detail/Credentials.h +++ b/src/xrpld/app/tx/detail/Credentials.h @@ -33,6 +33,9 @@ public: { } + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); @@ -54,6 +57,9 @@ public: { } + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); @@ -75,6 +81,9 @@ public: { } + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/DID.cpp b/src/xrpld/app/tx/detail/DID.cpp index 31ce7c8770..b38b207d36 100644 --- a/src/xrpld/app/tx/detail/DID.cpp +++ b/src/xrpld/app/tx/detail/DID.cpp @@ -18,10 +18,10 @@ //============================================================================== #include -#include -#include #include +#include +#include #include #include #include @@ -45,15 +45,6 @@ namespace ripple { NotTEC DIDSet::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureDID)) - return temDISABLED; - - if (ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - if (!ctx.tx.isFieldPresent(sfURI) && !ctx.tx.isFieldPresent(sfDIDDocument) && !ctx.tx.isFieldPresent(sfData)) return temEMPTY_DID; @@ -74,7 +65,7 @@ DIDSet::preflight(PreflightContext const& ctx) isTooLong(sfData, maxDIDAttestationLength)) return temMALFORMED; - return preflight2(ctx); + return tesSUCCESS; } TER @@ -174,16 +165,7 @@ DIDSet::doApply() NotTEC DIDDelete::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureDID)) - return temDISABLED; - - if (ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/DelegateSet.cpp b/src/xrpld/app/tx/detail/DelegateSet.cpp index ddeb01b399..e769f75d8a 100644 --- a/src/xrpld/app/tx/detail/DelegateSet.cpp +++ b/src/xrpld/app/tx/detail/DelegateSet.cpp @@ -18,9 +18,9 @@ //============================================================================== #include -#include #include +#include #include #include #include @@ -30,12 +30,6 @@ namespace ripple { NotTEC DelegateSet::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featurePermissionDelegation)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - auto const& permissions = ctx.tx.getFieldArray(sfPermissions); if (permissions.size() > permissionMaxSize) return temARRAY_TOO_LARGE; @@ -57,7 +51,7 @@ DelegateSet::preflight(PreflightContext const& ctx) return temMALFORMED; } - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/DeleteAccount.cpp b/src/xrpld/app/tx/detail/DeleteAccount.cpp index 4311aa79a8..565d938c83 100644 --- a/src/xrpld/app/tx/detail/DeleteAccount.cpp +++ b/src/xrpld/app/tx/detail/DeleteAccount.cpp @@ -17,7 +17,6 @@ */ //============================================================================== -#include #include #include #include @@ -25,35 +24,36 @@ #include #include #include -#include #include #include #include +#include +#include #include -#include #include #include #include +#include namespace ripple { +bool +DeleteAccount::checkExtraFeatures(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureDeletableAccounts)) + return false; + + if (ctx.tx.isFieldPresent(sfCredentialIDs) && + !ctx.rules.enabled(featureCredentials)) + return false; + + return true; +} + NotTEC DeleteAccount::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureDeletableAccounts)) - return temDISABLED; - - if (ctx.tx.isFieldPresent(sfCredentialIDs) && - !ctx.rules.enabled(featureCredentials)) - return temDISABLED; - - if (ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - if (ctx.tx[sfAccount] == ctx.tx[sfDestination]) // An account cannot be deleted and give itself the resulting XRP. return temDST_IS_SRC; @@ -62,14 +62,14 @@ DeleteAccount::preflight(PreflightContext const& ctx) !isTesSuccess(err)) return err; - return preflight2(ctx); + return tesSUCCESS; } XRPAmount DeleteAccount::calculateBaseFee(ReadView const& view, STTx const& tx) { // The fee required for AccountDelete is one owner reserve. - return view.fees().increment; + return calculateOwnerReserveFee(view, tx); } namespace { diff --git a/src/xrpld/app/tx/detail/DeleteAccount.h b/src/xrpld/app/tx/detail/DeleteAccount.h index c9d3305562..ee9db97d50 100644 --- a/src/xrpld/app/tx/detail/DeleteAccount.h +++ b/src/xrpld/app/tx/detail/DeleteAccount.h @@ -33,6 +33,9 @@ public: { } + static bool + checkExtraFeatures(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/DeleteOracle.cpp b/src/xrpld/app/tx/detail/DeleteOracle.cpp index 78e3d55230..7dba477aaa 100644 --- a/src/xrpld/app/tx/detail/DeleteOracle.cpp +++ b/src/xrpld/app/tx/detail/DeleteOracle.cpp @@ -18,8 +18,8 @@ //============================================================================== #include -#include +#include #include #include #include @@ -29,19 +29,7 @@ namespace ripple { NotTEC DeleteOracle::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featurePriceOracle)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfUniversalMask) - { - JLOG(ctx.j.debug()) << "Oracle Delete: invalid flags."; - return temINVALID_FLAG; - } - - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/DepositPreauth.cpp b/src/xrpld/app/tx/detail/DepositPreauth.cpp index f10f09b38f..236b59a173 100644 --- a/src/xrpld/app/tx/detail/DepositPreauth.cpp +++ b/src/xrpld/app/tx/detail/DepositPreauth.cpp @@ -17,11 +17,11 @@ */ //============================================================================== -#include #include -#include #include +#include +#include #include #include #include @@ -30,32 +30,29 @@ namespace ripple { +bool +DepositPreauth::checkExtraFeatures(PreflightContext const& ctx) +{ + bool const authArrPresent = ctx.tx.isFieldPresent(sfAuthorizeCredentials); + bool const unauthArrPresent = + ctx.tx.isFieldPresent(sfUnauthorizeCredentials); + bool const authCredPresent = authArrPresent || unauthArrPresent; + + if (authCredPresent && !ctx.rules.enabled(featureCredentials)) + return false; + + return true; +} + NotTEC DepositPreauth::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureDepositPreauth)) - return temDISABLED; - bool const authArrPresent = ctx.tx.isFieldPresent(sfAuthorizeCredentials); bool const unauthArrPresent = ctx.tx.isFieldPresent(sfUnauthorizeCredentials); int const authCredPresent = static_cast(authArrPresent) + static_cast(unauthArrPresent); - if (authCredPresent && !ctx.rules.enabled(featureCredentials)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - auto& tx = ctx.tx; - - if (tx.getFlags() & tfUniversalMask) - { - JLOG(ctx.j.trace()) << "Malformed transaction: Invalid flags set."; - return temINVALID_FLAG; - } - auto const optAuth = ctx.tx[~sfAuthorize]; auto const optUnauth = ctx.tx[~sfUnauthorize]; int const authPresent = static_cast(optAuth.has_value()) + @@ -102,7 +99,7 @@ DepositPreauth::preflight(PreflightContext const& ctx) return err; } - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/DepositPreauth.h b/src/xrpld/app/tx/detail/DepositPreauth.h index 76a7c08073..ead17742cd 100644 --- a/src/xrpld/app/tx/detail/DepositPreauth.h +++ b/src/xrpld/app/tx/detail/DepositPreauth.h @@ -33,6 +33,9 @@ public: { } + static bool + checkExtraFeatures(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/Escrow.cpp b/src/xrpld/app/tx/detail/Escrow.cpp index dd0ffac778..969fd4dd4c 100644 --- a/src/xrpld/app/tx/detail/Escrow.cpp +++ b/src/xrpld/app/tx/detail/Escrow.cpp @@ -17,17 +17,17 @@ */ //============================================================================== -#include #include #include #include #include #include -#include -#include #include #include +#include +#include +#include #include #include #include @@ -81,8 +81,8 @@ constexpr HashRouterFlags SF_CF_VALID = HashRouterFlags::PRIVATE6; TxConsequences EscrowCreate::makeTxConsequences(PreflightContext const& ctx) { - return TxConsequences{ - ctx.tx, isXRP(ctx.tx[sfAmount]) ? ctx.tx[sfAmount].xrp() : beast::zero}; + auto const amount = ctx.tx[sfAmount]; + return TxConsequences{ctx.tx, isXRP(amount) ? amount.xrp() : beast::zero}; } template @@ -118,15 +118,16 @@ escrowCreatePreflightHelper(PreflightContext const& ctx) return tesSUCCESS; } +std::uint32_t +EscrowCreate::getFlagsMask(PreflightContext const& ctx) +{ + // 0 means "Allow any flags" + return ctx.rules.enabled(fix1543) ? tfUniversalMask : 0; +} + NotTEC EscrowCreate::preflight(PreflightContext const& ctx) { - if (ctx.rules.enabled(fix1543) && ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - STAmount const amount{ctx.tx[sfAmount]}; if (!isXRP(amount)) { @@ -189,7 +190,7 @@ EscrowCreate::preflight(PreflightContext const& ctx) return temDISABLED; } - return preflight2(ctx); + return tesSUCCESS; } template @@ -538,6 +539,11 @@ EscrowCreate::doApply() (*slep)[~sfFinishAfter] = ctx_.tx[~sfFinishAfter]; (*slep)[~sfDestinationTag] = ctx_.tx[~sfDestinationTag]; + if (ctx_.view().rules().enabled(fixIncludeKeyletFields)) + { + (*slep)[sfSequence] = ctx_.tx.getSeqValue(); + } + if (ctx_.view().rules().enabled(featureTokenEscrow) && !isXRP(amount)) { auto const xferRate = transferRate(ctx_.view(), amount); @@ -624,19 +630,23 @@ checkCondition(Slice f, Slice c) return validate(*fulfillment, *condition); } +bool +EscrowFinish::checkExtraFeatures(PreflightContext const& ctx) +{ + return !ctx.tx.isFieldPresent(sfCredentialIDs) || + ctx.rules.enabled(featureCredentials); +} + +std::uint32_t +EscrowFinish::getFlagsMask(PreflightContext const& ctx) +{ + // 0 means "Allow any flags" + return ctx.rules.enabled(fix1543) ? tfUniversalMask : 0; +} + NotTEC EscrowFinish::preflight(PreflightContext const& ctx) { - if (ctx.rules.enabled(fix1543) && ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; - - if (ctx.tx.isFieldPresent(sfCredentialIDs) && - !ctx.rules.enabled(featureCredentials)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - auto const cb = ctx.tx[~sfCondition]; auto const fb = ctx.tx[~sfFulfillment]; @@ -645,13 +655,14 @@ EscrowFinish::preflight(PreflightContext const& ctx) if (static_cast(cb) != static_cast(fb)) return temMALFORMED; - // Verify the transaction signature. If it doesn't work - // then don't do any more work. - { - auto const ret = preflight2(ctx); - if (!isTesSuccess(ret)) - return ret; - } + return tesSUCCESS; +} + +NotTEC +EscrowFinish::preflightSigValidated(PreflightContext const& ctx) +{ + auto const cb = ctx.tx[~sfCondition]; + auto const fb = ctx.tx[~sfFulfillment]; if (cb && fb) { @@ -1007,8 +1018,13 @@ escrowUnlockApplyHelper( // compute balance to transfer finalAmt = amount.value() - xferFee; } - - return rippleUnlockEscrowMPT(view, sender, receiver, finalAmt, journal); + return rippleUnlockEscrowMPT( + view, + sender, + receiver, + finalAmt, + view.rules().enabled(fixTokenEscrowV1) ? amount : finalAmt, + journal); } TER @@ -1197,16 +1213,17 @@ EscrowFinish::doApply() //------------------------------------------------------------------------------ +std::uint32_t +EscrowCancel::getFlagsMask(PreflightContext const& ctx) +{ + // 0 means "Allow any flags" + return ctx.rules.enabled(fix1543) ? tfUniversalMask : 0; +} + NotTEC EscrowCancel::preflight(PreflightContext const& ctx) { - if (ctx.rules.enabled(fix1543) && ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - return preflight2(ctx); + return tesSUCCESS; } template diff --git a/src/xrpld/app/tx/detail/Escrow.h b/src/xrpld/app/tx/detail/Escrow.h index 2225c94f16..8956be2939 100644 --- a/src/xrpld/app/tx/detail/Escrow.h +++ b/src/xrpld/app/tx/detail/Escrow.h @@ -36,6 +36,9 @@ public: static TxConsequences makeTxConsequences(PreflightContext const& ctx); + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); @@ -57,9 +60,18 @@ public: { } + static bool + checkExtraFeatures(PreflightContext const& ctx); + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); + static NotTEC + preflightSigValidated(PreflightContext const& ctx); + static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); @@ -81,6 +93,9 @@ public: { } + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index da0dfc117f..87ca9ea6c1 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -19,23 +19,97 @@ #include #include -#include #include #include #include -#include -#include #include +#include +#include +#include #include -#include #include +#include #include #include +#include #include namespace ripple { +/* +assert(enforce) + +There are several asserts (or XRPL_ASSERTs) in this file that check a variable +named `enforce` when an invariant fails. At first glance, those asserts may look +incorrect, but they are not. + +Those asserts take advantage of two facts: +1. `asserts` are not (normally) executed in release builds. +2. Invariants should *never* fail, except in tests that specifically modify + the open ledger to break them. + +This makes `assert(enforce)` sort of a second-layer of invariant enforcement +aimed at _developers_. It's designed to fire if a developer writes code that +violates an invariant, and runs it in unit tests or a develop build that _does +not have the relevant amendments enabled_. It's intentionally a pain in the neck +so that bad code gets caught and fixed as early as possible. +*/ + +enum Privilege { + noPriv = + 0x0000, // The transaction can not do any of the enumerated operations + createAcct = + 0x0001, // The transaction can create a new ACCOUNT_ROOT object. + createPseudoAcct = 0x0002, // The transaction can create a pseudo account, + // which implies createAcct + mustDeleteAcct = + 0x0004, // The transaction must delete an ACCOUNT_ROOT object + mayDeleteAcct = 0x0008, // The transaction may delete an ACCOUNT_ROOT + // object, but does not have to + overrideFreeze = 0x0010, // The transaction can override some freeze rules + changeNFTCounts = 0x0020, // The transaction can mint or burn an NFT + createMPTIssuance = + 0x0040, // The transaction can create a new MPT issuance + destroyMPTIssuance = 0x0080, // The transaction can destroy an MPT issuance + mustAuthorizeMPT = 0x0100, // The transaction MUST create or delete an MPT + // object (except by issuer) + mayAuthorizeMPT = 0x0200, // The transaction MAY create or delete an MPT + // object (except by issuer) + mayDeleteMPT = + 0x0400, // The transaction MAY delete an MPT object. May not create. +}; +constexpr Privilege +operator|(Privilege lhs, Privilege rhs) +{ + return safe_cast( + safe_cast>(lhs) | + safe_cast>(rhs)); +} + +#pragma push_macro("TRANSACTION") +#undef TRANSACTION + +#define TRANSACTION(tag, value, name, delegatable, amendment, privileges, ...) \ + case tag: { \ + return (privileges) & priv; \ + } + +bool +hasPrivilege(STTx const& tx, Privilege priv) +{ + switch (tx.getTxnType()) + { +#include + // Deprecated types + default: + return false; + } +}; + +#undef TRANSACTION +#pragma pop_macro("TRANSACTION") + void TransactionFeeCheck::visitEntry( bool, @@ -379,10 +453,7 @@ AccountRootsNotDeleted::finalize( // transaction when the total AMM LP Tokens balance goes to 0. // A successful AccountDelete or AMMDelete MUST delete exactly // one account root. - if ((tx.getTxnType() == ttACCOUNT_DELETE || - tx.getTxnType() == ttAMM_DELETE || - tx.getTxnType() == ttVAULT_DELETE) && - result == tesSUCCESS) + if (hasPrivilege(tx, mustDeleteAcct) && result == tesSUCCESS) { if (accountsDeleted_ == 1) return true; @@ -399,9 +470,8 @@ AccountRootsNotDeleted::finalize( // A successful AMMWithdraw/AMMClawback MAY delete one account root // when the total AMM LP Tokens balance goes to 0. Not every AMM withdraw // deletes the AMM account, accountsDeleted_ is set if it is deleted. - if ((tx.getTxnType() == ttAMM_WITHDRAW || - tx.getTxnType() == ttAMM_CLAWBACK) && - result == tesSUCCESS && accountsDeleted_ == 1) + if (hasPrivilege(tx, mayDeleteAcct) && result == tesSUCCESS && + accountsDeleted_ == 1) return true; if (accountsDeleted_ == 0) @@ -436,7 +506,8 @@ AccountRootsDeletedClean::finalize( // feature is enabled. Enabled, or not, though, a fatal-level message will // be logged [[maybe_unused]] bool const enforce = - view.rules().enabled(featureInvariantsV1_1); + view.rules().enabled(featureInvariantsV1_1) || + view.rules().enabled(featureSingleAssetVault); auto const objectExists = [&view, enforce, &j](auto const& keylet) { (void)enforce; @@ -455,6 +526,8 @@ AccountRootsDeletedClean::finalize( JLOG(j.fatal()) << "Invariant failed: account deletion left behind a " << typeName << " object"; + // The comment above starting with "assert(enforce)" explains this + // assert. XRPL_ASSERT( enforce, "ripple::AccountRootsDeletedClean::finalize::objectExists : " @@ -489,11 +562,16 @@ AccountRootsDeletedClean::finalize( return false; } - // Keys directly stored in the AccountRoot object - if (auto const ammKey = accountSLE->at(~sfAMMID)) + // If the account is a pseudo account, then the linked object must + // also be deleted. e.g. AMM, Vault, etc. + for (auto const& field : getPseudoAccountFields()) { - if (objectExists(keylet::amm(*ammKey)) && enforce) - return false; + if (accountSLE->isFieldPresent(*field)) + { + auto const key = accountSLE->getFieldH256(*field); + if (objectExists(keylet::unchecked(key)) && enforce) + return false; + } } } @@ -513,41 +591,23 @@ LedgerEntryTypesMatch::visitEntry( if (after) { +#pragma push_macro("LEDGER_ENTRY") +#undef LEDGER_ENTRY + +#define LEDGER_ENTRY(tag, ...) case tag: + switch (after->getType()) { - case ltACCOUNT_ROOT: - case ltDELEGATE: - case ltDIR_NODE: - case ltRIPPLE_STATE: - case ltTICKET: - case ltSIGNER_LIST: - case ltOFFER: - case ltLEDGER_HASHES: - case ltAMENDMENTS: - case ltFEE_SETTINGS: - case ltESCROW: - case ltPAYCHAN: - case ltCHECK: - case ltDEPOSIT_PREAUTH: - case ltNEGATIVE_UNL: - case ltNFTOKEN_PAGE: - case ltNFTOKEN_OFFER: - case ltAMM: - case ltBRIDGE: - case ltXCHAIN_OWNED_CLAIM_ID: - case ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID: - case ltDID: - case ltORACLE: - case ltMPTOKEN_ISSUANCE: - case ltMPTOKEN: - case ltCREDENTIAL: - case ltPERMISSIONED_DOMAIN: - case ltVAULT: - break; +#include + + break; default: invalidTypeAdded_ = true; break; } + +#undef LEDGER_ENTRY +#pragma pop_macro("LEDGER_ENTRY") } } @@ -713,6 +773,8 @@ TransfersNotFrozen::finalize( // just in case so rippled doesn't crash in release. if (!issuerSle) { + // The comment above starting with "assert(enforce)" explains this + // assert. XRPL_ASSERT( enforce, "ripple::TransfersNotFrozen::finalize : enforce " @@ -901,7 +963,7 @@ TransfersNotFrozen::validateFrozenState( } // AMMClawbacks are allowed to override some freeze rules - if ((!isAMMLine || globalFreeze) && tx.getTxnType() == ttAMM_CLAWBACK) + if ((!isAMMLine || globalFreeze) && hasPrivilege(tx, overrideFreeze)) { JLOG(j.debug()) << "Invariant check allowing funds to be moved " << (change.balanceChangeSign > 0 ? "to" : "from") @@ -912,6 +974,7 @@ TransfersNotFrozen::validateFrozenState( JLOG(j.fatal()) << "Invariant failed: Attempting to move frozen funds for " << tx.getTransactionID(); + // The comment above starting with "assert(enforce)" explains this assert. XRPL_ASSERT( enforce, "ripple::TransfersNotFrozen::validateFrozenState : enforce " @@ -961,17 +1024,12 @@ ValidNewAccountRoot::finalize( } // From this point on we know exactly one account was created. - if ((tx.getTxnType() == ttPAYMENT || tx.getTxnType() == ttAMM_CREATE || - tx.getTxnType() == ttVAULT_CREATE || - tx.getTxnType() == ttXCHAIN_ADD_CLAIM_ATTESTATION || - tx.getTxnType() == ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION) && - result == tesSUCCESS) + if (hasPrivilege(tx, createAcct | createPseudoAcct) && result == tesSUCCESS) { bool const pseudoAccount = (pseudoAccount_ && view.rules().enabled(featureSingleAssetVault)); - if (pseudoAccount && tx.getTxnType() != ttAMM_CREATE && - tx.getTxnType() != ttVAULT_CREATE) + if (pseudoAccount && !hasPrivilege(tx, createPseudoAcct)) { JLOG(j.fatal()) << "Invariant failed: pseudo-account created by a " "wrong transaction type"; @@ -1010,7 +1068,7 @@ ValidNewAccountRoot::finalize( JLOG(j.fatal()) << "Invariant failed: account root created illegally"; return false; -} +} // namespace ripple //------------------------------------------------------------------------------ @@ -1205,8 +1263,7 @@ NFTokenCountTracking::finalize( ReadView const& view, beast::Journal const& j) { - if (TxType const txType = tx.getTxnType(); - txType != ttNFTOKEN_MINT && txType != ttNFTOKEN_BURN) + if (!hasPrivilege(tx, changeNFTCounts)) { if (beforeMintedTotal != afterMintedTotal) { @@ -1391,13 +1448,12 @@ ValidMPTIssuance::finalize( STTx const& tx, TER const result, XRPAmount const _fee, - ReadView const& _view, + ReadView const& view, beast::Journal const& j) { if (result == tesSUCCESS) { - if (tx.getTxnType() == ttMPTOKEN_ISSUANCE_CREATE || - tx.getTxnType() == ttVAULT_CREATE) + if (hasPrivilege(tx, createMPTIssuance)) { if (mptIssuancesCreated_ == 0) { @@ -1418,8 +1474,7 @@ ValidMPTIssuance::finalize( return mptIssuancesCreated_ == 1 && mptIssuancesDeleted_ == 0; } - if (tx.getTxnType() == ttMPTOKEN_ISSUANCE_DESTROY || - tx.getTxnType() == ttVAULT_DELETE) + if (hasPrivilege(tx, destroyMPTIssuance)) { if (mptIssuancesDeleted_ == 0) { @@ -1440,8 +1495,17 @@ ValidMPTIssuance::finalize( return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 1; } - if (tx.getTxnType() == ttMPTOKEN_AUTHORIZE || - tx.getTxnType() == ttVAULT_DEPOSIT) + // ttESCROW_FINISH may authorize an MPT, but it can't have the + // mayAuthorizeMPT privilege, because that may cause + // non-amendment-gated side effects. + bool const enforceEscrowFinish = (tx.getTxnType() == ttESCROW_FINISH) && + (view.rules().enabled(featureSingleAssetVault) + /* + TODO: Uncomment when LendingProtocol is defined + || view.rules().enabled(featureLendingProtocol)*/ + ); + if (hasPrivilege(tx, mustAuthorizeMPT | mayAuthorizeMPT) || + enforceEscrowFinish) { bool const submittedByIssuer = tx.isFieldPresent(sfHolder); @@ -1467,7 +1531,7 @@ ValidMPTIssuance::finalize( return false; } else if ( - !submittedByIssuer && (tx.getTxnType() != ttVAULT_DEPOSIT) && + !submittedByIssuer && hasPrivilege(tx, mustAuthorizeMPT) && (mptokensCreated_ + mptokensDeleted_ != 1)) { // if the holder submitted this tx, then a mptoken must be @@ -1480,41 +1544,21 @@ ValidMPTIssuance::finalize( return true; } - - if (tx.getTxnType() == ttMPTOKEN_ISSUANCE_SET) + if (tx.getTxnType() == ttESCROW_FINISH) { - if (mptIssuancesDeleted_ > 0) - { - JLOG(j.fatal()) << "Invariant failed: MPT issuance set " - "succeeded while removing MPT issuances"; - } - else if (mptIssuancesCreated_ > 0) - { - JLOG(j.fatal()) << "Invariant failed: MPT issuance set " - "succeeded while creating MPT issuances"; - } - else if (mptokensDeleted_ > 0) - { - JLOG(j.fatal()) << "Invariant failed: MPT issuance set " - "succeeded while removing MPTokens"; - } - else if (mptokensCreated_ > 0) - { - JLOG(j.fatal()) << "Invariant failed: MPT issuance set " - "succeeded while creating MPTokens"; - } - - return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0 && - mptokensCreated_ == 0 && mptokensDeleted_ == 0; + // ttESCROW_FINISH may authorize an MPT, but it can't have the + // mayAuthorizeMPT privilege, because that may cause + // non-amendment-gated side effects. + XRPL_ASSERT_PARTS( + !enforceEscrowFinish, + "ripple::ValidMPTIssuance::finalize", + "not escrow finish tx"); + return true; } - if (tx.getTxnType() == ttESCROW_FINISH) - return true; - - if ((tx.getTxnType() == ttVAULT_CLAWBACK || - tx.getTxnType() == ttVAULT_WITHDRAW) && - mptokensDeleted_ == 1 && mptokensCreated_ == 0 && - mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0) + if (hasPrivilege(tx, mayDeleteMPT) && mptokensDeleted_ == 1 && + mptokensCreated_ == 0 && mptIssuancesCreated_ == 0 && + mptIssuancesDeleted_ == 0) return true; } @@ -1640,6 +1684,104 @@ ValidPermissionedDomain::finalize( (sleStatus_[1] ? check(*sleStatus_[1], j) : true); } +//------------------------------------------------------------------------------ + +void +ValidPseudoAccounts::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (isDelete) + // Deletion is ignored + return; + + if (after && after->getType() == ltACCOUNT_ROOT) + { + bool const isPseudo = [&]() { + // isPseudoAccount checks that any of the pseudo-account fields are + // set. + if (isPseudoAccount(after)) + return true; + // Not all pseudo-accounts have a zero sequence, but all accounts + // with a zero sequence had better be pseudo-accounts. + if (after->at(sfSequence) == 0) + return true; + + return false; + }(); + if (isPseudo) + { + // Pseudo accounts must have the following properties: + // 1. Exactly one of the pseudo-account fields is set. + // 2. The sequence number is not changed. + // 3. The lsfDisableMaster, lsfDefaultRipple, and lsfDepositAuth + // flags are set. + // 4. The RegularKey is not set. + { + std::vector const& fields = + getPseudoAccountFields(); + + auto const numFields = std::count_if( + fields.begin(), + fields.end(), + [&after](SField const* sf) -> bool { + return after->isFieldPresent(*sf); + }); + if (numFields != 1) + { + std::stringstream error; + error << "pseudo-account has " << numFields + << " pseudo-account fields set"; + errors_.emplace_back(error.str()); + } + } + if (before && before->at(sfSequence) != after->at(sfSequence)) + { + errors_.emplace_back("pseudo-account sequence changed"); + } + if (!after->isFlag( + lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth)) + { + errors_.emplace_back("pseudo-account flags are not set"); + } + if (after->isFieldPresent(sfRegularKey)) + { + errors_.emplace_back("pseudo-account has a regular key"); + } + } + } +} + +bool +ValidPseudoAccounts::finalize( + STTx const& tx, + TER const, + XRPAmount const, + ReadView const& view, + beast::Journal const& j) +{ + bool const enforce = view.rules().enabled(featureSingleAssetVault); + + // The comment above starting with "assert(enforce)" explains this assert. + XRPL_ASSERT( + errors_.empty() || enforce, + "ripple::ValidPseudoAccounts::finalize : no bad " + "changes or enforce invariant"); + if (!errors_.empty()) + { + for (auto const& error : errors_) + { + JLOG(j.fatal()) << "Invariant failed: " << error; + } + if (enforce) + return false; + } + return true; +} + +//------------------------------------------------------------------------------ + void ValidPermissionedDEX::visitEntry( bool, diff --git a/src/xrpld/app/tx/detail/InvariantCheck.h b/src/xrpld/app/tx/detail/InvariantCheck.h index 529c05ce0e..5444f2f3a9 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.h +++ b/src/xrpld/app/tx/detail/InvariantCheck.h @@ -619,6 +619,34 @@ public: beast::Journal const&); }; +/** + * @brief Invariants: Pseudo-accounts have valid and consisent properties + * + * Pseudo-accounts have certain properties, and some of those properties are + * unique to pseudo-accounts. Check that all pseudo-accounts are following the + * rules, and that only pseudo-accounts look like pseudo-accounts. + * + */ +class ValidPseudoAccounts +{ + std::vector errors_; + +public: + void + visitEntry( + bool, + std::shared_ptr const&, + std::shared_ptr const&); + + bool + finalize( + STTx const&, + TER const, + XRPAmount const, + ReadView const&, + beast::Journal const&); +}; + class ValidPermissionedDEX { bool regularOffers_ = false; @@ -725,7 +753,8 @@ using InvariantChecks = std::tuple< ValidMPTIssuance, ValidPermissionedDomain, ValidPermissionedDEX, - ValidAMM>; + ValidAMM, + ValidPseudoAccounts>; /** * @brief get a tuple of all invariant checks diff --git a/src/xrpld/app/tx/detail/LedgerStateFix.cpp b/src/xrpld/app/tx/detail/LedgerStateFix.cpp index 008d9b6ae7..6059e15313 100644 --- a/src/xrpld/app/tx/detail/LedgerStateFix.cpp +++ b/src/xrpld/app/tx/detail/LedgerStateFix.cpp @@ -19,8 +19,8 @@ #include #include -#include +#include #include #include #include @@ -30,15 +30,6 @@ namespace ripple { NotTEC LedgerStateFix::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(fixNFTokenPageLinks)) - return temDISABLED; - - if (ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - switch (ctx.tx[sfLedgerFixType]) { case FixType::nfTokenPageLink: @@ -50,7 +41,7 @@ LedgerStateFix::preflight(PreflightContext const& ctx) return tefINVALID_LEDGER_FIX_TYPE; } - return preflight2(ctx); + return tesSUCCESS; } XRPAmount @@ -58,7 +49,7 @@ LedgerStateFix::calculateBaseFee(ReadView const& view, STTx const& tx) { // The fee required for LedgerStateFix is one owner reserve, just like // the fee for AccountDelete. - return view.fees().increment; + return calculateOwnerReserveFee(view, tx); } TER diff --git a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp index 77fe19a287..edeb12e5c0 100644 --- a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp +++ b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp @@ -18,30 +18,27 @@ //============================================================================== #include -#include +#include #include #include #include namespace ripple { +std::uint32_t +MPTokenAuthorize::getFlagsMask(PreflightContext const& ctx) +{ + return tfMPTokenAuthorizeMask; +} + NotTEC MPTokenAuthorize::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureMPTokensV1)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfMPTokenAuthorizeMask) - return temINVALID_FLAG; - if (ctx.tx[sfAccount] == ctx.tx[~sfHolder]) return temMALFORMED; - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/MPTokenAuthorize.h b/src/xrpld/app/tx/detail/MPTokenAuthorize.h index 85e8edcf9f..43a962e24e 100644 --- a/src/xrpld/app/tx/detail/MPTokenAuthorize.h +++ b/src/xrpld/app/tx/detail/MPTokenAuthorize.h @@ -42,6 +42,9 @@ public: { } + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp index 556fe1d978..4225e9477b 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp @@ -18,32 +18,46 @@ //============================================================================== #include -#include +#include #include #include namespace ripple { -NotTEC -MPTokenIssuanceCreate::preflight(PreflightContext const& ctx) +bool +MPTokenIssuanceCreate::checkExtraFeatures(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureMPTokensV1)) - return temDISABLED; - if (ctx.tx.isFieldPresent(sfDomainID) && !(ctx.rules.enabled(featurePermissionedDomains) && ctx.rules.enabled(featureSingleAssetVault))) - return temDISABLED; + return false; + + if (ctx.tx.isFieldPresent(sfMutableFlags) && + !ctx.rules.enabled(featureDynamicMPT)) + return false; if (ctx.tx.getFlags() & tfMPTNoConfidentialTransfer && !ctx.rules.enabled(featureConfidentialTransfer)) - return temDISABLED; + return false; - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; + return true; +} - if (ctx.tx.getFlags() & tfMPTokenIssuanceCreateMask) +std::uint32_t +MPTokenIssuanceCreate::getFlagsMask(PreflightContext const& ctx) +{ + // This mask is only compared against sfFlags + return tfMPTokenIssuanceCreateMask; +} + +NotTEC +MPTokenIssuanceCreate::preflight(PreflightContext const& ctx) +{ + // If the mutable flags field is included, at least one flag must be + // specified. + if (auto const mutableFlags = ctx.tx[~sfMutableFlags]; mutableFlags && + (!*mutableFlags || *mutableFlags & tmfMPTokenIssuanceCreateMutableMask)) return temINVALID_FLAG; if (auto const fee = ctx.tx[~sfTransferFee]) @@ -83,7 +97,7 @@ MPTokenIssuanceCreate::preflight(PreflightContext const& ctx) if (maxAmt > maxMPTokenAmount) return temMALFORMED; } - return preflight2(ctx); + return tesSUCCESS; } Expected @@ -136,6 +150,9 @@ MPTokenIssuanceCreate::create( if (args.domainId) (*mptIssuance)[sfDomainID] = *args.domainId; + if (args.mutableFlags) + (*mptIssuance)[sfMutableFlags] = *args.mutableFlags; + view.insert(mptIssuance); } @@ -162,6 +179,7 @@ MPTokenIssuanceCreate::doApply() .transferFee = tx[~sfTransferFee], .metadata = tx[~sfMPTokenMetadata], .domainId = tx[~sfDomainID], + .mutableFlags = tx[~sfMutableFlags], }); return result ? tesSUCCESS : result.error(); } diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.h b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.h index ea01908dff..842ed88641 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.h +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.h @@ -38,6 +38,7 @@ struct MPTCreateArgs std::optional transferFee{}; std::optional const& metadata{}; std::optional domainId{}; + std::optional mutableFlags{}; }; class MPTokenIssuanceCreate : public Transactor @@ -49,6 +50,12 @@ public: { } + static bool + checkExtraFeatures(PreflightContext const& ctx); + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp index e2b87dbd79..4c502f1106 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp @@ -18,27 +18,23 @@ //============================================================================== #include -#include +#include #include #include namespace ripple { +std::uint32_t +MPTokenIssuanceDestroy::getFlagsMask(PreflightContext const& ctx) +{ + return tfMPTokenIssuanceDestroyMask; +} + NotTEC MPTokenIssuanceDestroy::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureMPTokensV1)) - return temDISABLED; - - // check flags - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfMPTokenIssuanceDestroyMask) - return temINVALID_FLAG; - - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.h b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.h index 69abb99feb..2cebdb7352 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.h +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.h @@ -33,6 +33,9 @@ public: { } + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp index e05862af37..6fb87711c8 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp @@ -26,30 +26,62 @@ namespace ripple { +bool +MPTokenIssuanceSet::checkExtraFeatures(PreflightContext const& ctx) +{ + return !ctx.tx.isFieldPresent(sfDomainID) || + (ctx.rules.enabled(featurePermissionedDomains) && + ctx.rules.enabled(featureSingleAssetVault)); +} + +std::uint32_t +MPTokenIssuanceSet::getFlagsMask(PreflightContext const& ctx) +{ + return tfMPTokenIssuanceSetMask; +} + +// Maps set/clear mutable flags in an MPTokenIssuanceSet transaction to the +// corresponding ledger mutable flags that control whether the change is +// allowed. +struct MPTMutabilityFlags +{ + std::uint32_t setFlag; + std::uint32_t clearFlag; + std::uint32_t canMutateFlag; +}; + +static constexpr std::array mptMutabilityFlags = { + {{tmfMPTSetCanLock, tmfMPTClearCanLock, lsmfMPTCanMutateCanLock}, + {tmfMPTSetRequireAuth, + tmfMPTClearRequireAuth, + lsmfMPTCanMutateRequireAuth}, + {tmfMPTSetCanEscrow, tmfMPTClearCanEscrow, lsmfMPTCanMutateCanEscrow}, + {tmfMPTSetCanTrade, tmfMPTClearCanTrade, lsmfMPTCanMutateCanTrade}, + {tmfMPTSetCanTransfer, + tmfMPTClearCanTransfer, + lsmfMPTCanMutateCanTransfer}, + {tmfMPTSetCanClawback, + tmfMPTClearCanClawback, + lsmfMPTCanMutateCanClawback}}}; + NotTEC MPTokenIssuanceSet::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureMPTokensV1)) - return temDISABLED; + auto const mutableFlags = ctx.tx[~sfMutableFlags]; + auto const metadata = ctx.tx[~sfMPTokenMetadata]; + auto const transferFee = ctx.tx[~sfTransferFee]; + auto const isMutate = mutableFlags || metadata || transferFee; - if (ctx.tx.isFieldPresent(sfDomainID) && - !(ctx.rules.enabled(featurePermissionedDomains) && - ctx.rules.enabled(featureSingleAssetVault))) + if (isMutate && !ctx.rules.enabled(featureDynamicMPT)) return temDISABLED; if (ctx.tx.isFieldPresent(sfDomainID) && ctx.tx.isFieldPresent(sfHolder)) return temMALFORMED; - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - auto const txFlags = ctx.tx.getFlags(); - // check flags - if (txFlags & tfMPTokenIssuanceSetMask) - return temINVALID_FLAG; // fails if both flags are set - else if ((txFlags & tfMPTLock) && (txFlags & tfMPTUnlock)) + if ((txFlags & tfMPTLock) && (txFlags & tfMPTUnlock)) return temINVALID_FLAG; auto const accountID = ctx.tx[sfAccount]; @@ -57,14 +89,55 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx) if (holderID && accountID == holderID) return temMALFORMED; - if (ctx.rules.enabled(featureSingleAssetVault)) + if (ctx.rules.enabled(featureSingleAssetVault) || + ctx.rules.enabled(featureDynamicMPT)) { // Is this transaction actually changing anything ? - if (txFlags == 0 && !ctx.tx.isFieldPresent(sfDomainID)) + if (txFlags == 0 && !ctx.tx.isFieldPresent(sfDomainID) && !isMutate) return temMALFORMED; } - return preflight2(ctx); + if (ctx.rules.enabled(featureDynamicMPT)) + { + // Holder field is not allowed when mutating MPTokenIssuance + if (isMutate && holderID) + return temMALFORMED; + + // Can not set flags when mutating MPTokenIssuance + if (isMutate && (txFlags & tfUniversalMask)) + return temMALFORMED; + + if (transferFee && *transferFee > maxTransferFee) + return temBAD_TRANSFER_FEE; + + if (metadata && metadata->length() > maxMPTokenMetadataLength) + return temMALFORMED; + + if (mutableFlags) + { + if (!*mutableFlags || + (*mutableFlags & tmfMPTokenIssuanceSetMutableMask)) + return temINVALID_FLAG; + + // Can not set and clear the same flag + if (std::any_of( + mptMutabilityFlags.begin(), + mptMutabilityFlags.end(), + [mutableFlags](auto const& f) { + return (*mutableFlags & f.setFlag) && + (*mutableFlags & f.clearFlag); + })) + return temINVALID_FLAG; + + // Trying to set a non-zero TransferFee and clear MPTCanTransfer + // in the same transaction is not allowed. + if (transferFee.value_or(0) && + (*mutableFlags & tmfMPTClearCanTransfer)) + return temMALFORMED; + } + } + + return tesSUCCESS; } TER @@ -116,7 +189,8 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx) if (!sleMptIssuance->isFlag(lsfMPTCanLock)) { // For readability two separate `if` rather than `||` of two conditions - if (!ctx.view.rules().enabled(featureSingleAssetVault)) + if (!ctx.view.rules().enabled(featureSingleAssetVault) && + !ctx.view.rules().enabled(featureDynamicMPT)) return tecNO_PERMISSION; else if (ctx.tx.isFlag(tfMPTLock) || ctx.tx.isFlag(tfMPTUnlock)) return tecNO_PERMISSION; @@ -152,6 +226,44 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx) } } + // sfMutableFlags is soeDEFAULT, defaulting to 0 if not specified on + // the ledger. + auto const currentMutableFlags = + sleMptIssuance->getFieldU32(sfMutableFlags); + + auto isMutableFlag = [&](std::uint32_t mutableFlag) -> bool { + return currentMutableFlags & mutableFlag; + }; + + if (auto const mutableFlags = ctx.tx[~sfMutableFlags]) + { + if (std::any_of( + mptMutabilityFlags.begin(), + mptMutabilityFlags.end(), + [mutableFlags, &isMutableFlag](auto const& f) { + return !isMutableFlag(f.canMutateFlag) && + ((*mutableFlags & (f.setFlag | f.clearFlag))); + })) + return tecNO_PERMISSION; + } + + if (!isMutableFlag(lsmfMPTCanMutateMetadata) && + ctx.tx.isFieldPresent(sfMPTokenMetadata)) + return tecNO_PERMISSION; + + if (auto const fee = ctx.tx[~sfTransferFee]) + { + // A non-zero TransferFee is only valid if the lsfMPTCanTransfer flag + // was previously enabled (at issuance or via a prior mutation). Setting + // it by tmfMPTSetCanTransfer in the current transaction does not meet + // this requirement. + if (fee > 0u && !sleMptIssuance->isFlag(lsfMPTCanTransfer)) + return tecNO_PERMISSION; + + if (!isMutableFlag(lsmfMPTCanMutateTransferFee)) + return tecNO_PERMISSION; + } + return tesSUCCESS; } @@ -180,9 +292,47 @@ MPTokenIssuanceSet::doApply() else if (txFlags & tfMPTUnlock) flagsOut &= ~lsfMPTLocked; + if (auto const mutableFlags = ctx_.tx[~sfMutableFlags].value_or(0)) + { + for (auto const& f : mptMutabilityFlags) + { + if (mutableFlags & f.setFlag) + flagsOut |= f.canMutateFlag; + else if (mutableFlags & f.clearFlag) + flagsOut &= ~f.canMutateFlag; + } + + if (mutableFlags & tmfMPTClearCanTransfer) + { + // If the lsfMPTCanTransfer flag is being cleared, then also clear + // the TransferFee field. + sle->makeFieldAbsent(sfTransferFee); + } + } + if (flagsIn != flagsOut) sle->setFieldU32(sfFlags, flagsOut); + if (auto const transferFee = ctx_.tx[~sfTransferFee]) + { + // TransferFee uses soeDEFAULT style: + // - If the field is absent, it is interpreted as 0. + // - If the field is present, it must be non-zero. + // Therefore, when TransferFee is 0, the field should be removed. + if (transferFee == 0) + sle->makeFieldAbsent(sfTransferFee); + else + sle->setFieldU16(sfTransferFee, *transferFee); + } + + if (auto const metadata = ctx_.tx[~sfMPTokenMetadata]) + { + if (metadata->empty()) + sle->makeFieldAbsent(sfMPTokenMetadata); + else + sle->setFieldVL(sfMPTokenMetadata, *metadata); + } + if (domainID) { // This is enforced in preflight. diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.h b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.h index 5b3db0e75b..f63812097e 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.h +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.h @@ -33,6 +33,12 @@ public: { } + static bool + checkExtraFeatures(PreflightContext const& ctx); + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp b/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp index ab74e5ac39..3b4a27ffd7 100644 --- a/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp +++ b/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp @@ -19,26 +19,23 @@ #include #include -#include +#include #include #include #include namespace ripple { +std::uint32_t +NFTokenAcceptOffer::getFlagsMask(PreflightContext const& ctx) +{ + return tfNFTokenAcceptOfferMask; +} + NotTEC NFTokenAcceptOffer::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureNonFungibleTokensV1)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfNFTokenAcceptOfferMask) - return temINVALID_FLAG; - auto const bo = ctx.tx[~sfNFTokenBuyOffer]; auto const so = ctx.tx[~sfNFTokenSellOffer]; @@ -57,7 +54,7 @@ NFTokenAcceptOffer::preflight(PreflightContext const& ctx) return temMALFORMED; } - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/NFTokenAcceptOffer.h b/src/xrpld/app/tx/detail/NFTokenAcceptOffer.h index dff3febbb2..995581d1ff 100644 --- a/src/xrpld/app/tx/detail/NFTokenAcceptOffer.h +++ b/src/xrpld/app/tx/detail/NFTokenAcceptOffer.h @@ -51,6 +51,9 @@ public: { } + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/NFTokenBurn.cpp b/src/xrpld/app/tx/detail/NFTokenBurn.cpp index 947a663f92..cb1b564402 100644 --- a/src/xrpld/app/tx/detail/NFTokenBurn.cpp +++ b/src/xrpld/app/tx/detail/NFTokenBurn.cpp @@ -29,16 +29,7 @@ namespace ripple { NotTEC NFTokenBurn::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureNonFungibleTokensV1)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; - - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/NFTokenCancelOffer.cpp b/src/xrpld/app/tx/detail/NFTokenCancelOffer.cpp index f6072bc953..86e804b1a5 100644 --- a/src/xrpld/app/tx/detail/NFTokenCancelOffer.cpp +++ b/src/xrpld/app/tx/detail/NFTokenCancelOffer.cpp @@ -19,8 +19,8 @@ #include #include -#include +#include #include #include @@ -28,18 +28,15 @@ namespace ripple { +std::uint32_t +NFTokenCancelOffer::getFlagsMask(PreflightContext const& ctx) +{ + return tfNFTokenCancelOfferMask; +} + NotTEC NFTokenCancelOffer::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureNonFungibleTokensV1)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfNFTokenCancelOfferMask) - return temINVALID_FLAG; - if (auto const& ids = ctx.tx[sfNFTokenOffers]; ids.empty() || (ids.size() > maxTokenOfferCancelCount)) return temMALFORMED; @@ -51,7 +48,7 @@ NFTokenCancelOffer::preflight(PreflightContext const& ctx) if (std::adjacent_find(ids.begin(), ids.end()) != ids.end()) return temMALFORMED; - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/NFTokenCancelOffer.h b/src/xrpld/app/tx/detail/NFTokenCancelOffer.h index d460675711..b35be0e757 100644 --- a/src/xrpld/app/tx/detail/NFTokenCancelOffer.h +++ b/src/xrpld/app/tx/detail/NFTokenCancelOffer.h @@ -33,6 +33,9 @@ public: { } + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/NFTokenCreateOffer.cpp b/src/xrpld/app/tx/detail/NFTokenCreateOffer.cpp index 8e1a026415..2a02fed797 100644 --- a/src/xrpld/app/tx/detail/NFTokenCreateOffer.cpp +++ b/src/xrpld/app/tx/detail/NFTokenCreateOffer.cpp @@ -19,27 +19,24 @@ #include #include -#include +#include #include #include namespace ripple { +std::uint32_t +NFTokenCreateOffer::getFlagsMask(PreflightContext const& ctx) +{ + return tfNFTokenCreateOfferMask; +} + NotTEC NFTokenCreateOffer::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureNonFungibleTokensV1)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - auto const txFlags = ctx.tx.getFlags(); - if (txFlags & tfNFTokenCreateOfferMask) - return temINVALID_FLAG; - auto const nftFlags = nft::getFlags(ctx.tx[sfNFTokenID]); // Use implementation shared with NFTokenMint @@ -55,7 +52,7 @@ NFTokenCreateOffer::preflight(PreflightContext const& ctx) !isTesSuccess(notTec)) return notTec; - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/NFTokenCreateOffer.h b/src/xrpld/app/tx/detail/NFTokenCreateOffer.h index 075a5a712f..0a1c631298 100644 --- a/src/xrpld/app/tx/detail/NFTokenCreateOffer.h +++ b/src/xrpld/app/tx/detail/NFTokenCreateOffer.h @@ -33,6 +33,9 @@ public: { } + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/NFTokenMint.cpp b/src/xrpld/app/tx/detail/NFTokenMint.cpp index 42b551b3a4..8149d3b59d 100644 --- a/src/xrpld/app/tx/detail/NFTokenMint.cpp +++ b/src/xrpld/app/tx/detail/NFTokenMint.cpp @@ -18,9 +18,9 @@ //============================================================================== #include -#include #include +#include #include #include #include @@ -38,22 +38,23 @@ extractNFTokenFlagsFromTxFlags(std::uint32_t txFlags) return static_cast(txFlags & 0x0000FFFF); } -NotTEC -NFTokenMint::preflight(PreflightContext const& ctx) +static bool +hasOfferFields(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureNonFungibleTokensV1)) - return temDISABLED; - - bool const hasOfferFields = ctx.tx.isFieldPresent(sfAmount) || + return ctx.tx.isFieldPresent(sfAmount) || ctx.tx.isFieldPresent(sfDestination) || ctx.tx.isFieldPresent(sfExpiration); +} - if (!ctx.rules.enabled(featureNFTokenMintOffer) && hasOfferFields) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; +bool +NFTokenMint::checkExtraFeatures(PreflightContext const& ctx) +{ + return ctx.rules.enabled(featureNFTokenMintOffer) || !hasOfferFields(ctx); +} +std::uint32_t +NFTokenMint::getFlagsMask(PreflightContext const& ctx) +{ // Prior to fixRemoveNFTokenAutoTrustLine, transfer of an NFToken between // accounts allowed a TrustLine to be added to the issuer of that token // without explicit permission from that issuer. This was enabled by @@ -67,7 +68,7 @@ NFTokenMint::preflight(PreflightContext const& ctx) // The fixRemoveNFTokenAutoTrustLine amendment disables minting with the // tfTrustLine flag as a way to prevent the attack. But until the // amendment passes we still need to keep the old behavior available. - std::uint32_t const NFTokenMintMask = + std::uint32_t const nfTokenMintMask = ctx.rules.enabled(fixRemoveNFTokenAutoTrustLine) // if featureDynamicNFT enabled then new flag allowing mutable URI // available @@ -76,9 +77,12 @@ NFTokenMint::preflight(PreflightContext const& ctx) : ctx.rules.enabled(featureDynamicNFT) ? tfNFTokenMintOldMaskWithMutable : tfNFTokenMintOldMask; - if (ctx.tx.getFlags() & NFTokenMintMask) - return temINVALID_FLAG; + return nfTokenMintMask; +} +NotTEC +NFTokenMint::preflight(PreflightContext const& ctx) +{ if (auto const f = ctx.tx[~sfTransferFee]) { if (f > maxTransferFee) @@ -100,7 +104,7 @@ NFTokenMint::preflight(PreflightContext const& ctx) return temMALFORMED; } - if (hasOfferFields) + if (hasOfferFields(ctx)) { // The Amount field must be present if either the Destination or // Expiration fields are present. @@ -123,7 +127,7 @@ NFTokenMint::preflight(PreflightContext const& ctx) } } - return preflight2(ctx); + return tesSUCCESS; } uint256 diff --git a/src/xrpld/app/tx/detail/NFTokenMint.h b/src/xrpld/app/tx/detail/NFTokenMint.h index f606120c54..1606514559 100644 --- a/src/xrpld/app/tx/detail/NFTokenMint.h +++ b/src/xrpld/app/tx/detail/NFTokenMint.h @@ -36,6 +36,12 @@ public: { } + static bool + checkExtraFeatures(PreflightContext const& ctx); + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/NFTokenModify.cpp b/src/xrpld/app/tx/detail/NFTokenModify.cpp index a3803c423b..6ae095411b 100644 --- a/src/xrpld/app/tx/detail/NFTokenModify.cpp +++ b/src/xrpld/app/tx/detail/NFTokenModify.cpp @@ -25,19 +25,15 @@ namespace ripple { +bool +NFTokenModify::checkExtraFeatures(PreflightContext const& ctx) +{ + return ctx.rules.enabled(featureNonFungibleTokensV1_1); +} + NotTEC NFTokenModify::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureNonFungibleTokensV1_1) || - !ctx.rules.enabled(featureDynamicNFT)) - return temDISABLED; - - if (NotTEC const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; - if (auto owner = ctx.tx[~sfOwner]; owner == ctx.tx[sfAccount]) return temMALFORMED; @@ -47,7 +43,7 @@ NFTokenModify::preflight(PreflightContext const& ctx) return temMALFORMED; } - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/NFTokenModify.h b/src/xrpld/app/tx/detail/NFTokenModify.h index 0d1e72ade1..04784381fb 100644 --- a/src/xrpld/app/tx/detail/NFTokenModify.h +++ b/src/xrpld/app/tx/detail/NFTokenModify.h @@ -33,6 +33,9 @@ public: { } + static bool + checkExtraFeatures(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/NFTokenUtils.cpp b/src/xrpld/app/tx/detail/NFTokenUtils.cpp index 4866a3b385..ad3e6f4d35 100644 --- a/src/xrpld/app/tx/detail/NFTokenUtils.cpp +++ b/src/xrpld/app/tx/detail/NFTokenUtils.cpp @@ -18,10 +18,10 @@ //============================================================================== #include -#include -#include #include +#include +#include #include #include #include diff --git a/src/xrpld/app/tx/detail/NFTokenUtils.h b/src/xrpld/app/tx/detail/NFTokenUtils.h index 7ee0541984..6d33d4ec1a 100644 --- a/src/xrpld/app/tx/detail/NFTokenUtils.h +++ b/src/xrpld/app/tx/detail/NFTokenUtils.h @@ -21,9 +21,9 @@ #define RIPPLE_TX_IMPL_DETAILS_NFTOKENUTILS_H_INCLUDED #include -#include #include +#include #include #include #include diff --git a/src/xrpld/app/tx/detail/Offer.h b/src/xrpld/app/tx/detail/Offer.h index d6ff4c7699..c214bea23f 100644 --- a/src/xrpld/app/tx/detail/Offer.h +++ b/src/xrpld/app/tx/detail/Offer.h @@ -20,10 +20,9 @@ #ifndef RIPPLE_APP_BOOK_OFFER_H_INCLUDED #define RIPPLE_APP_BOOK_OFFER_H_INCLUDED -#include - #include #include +#include #include #include #include diff --git a/src/xrpld/app/tx/detail/OfferStream.cpp b/src/xrpld/app/tx/detail/OfferStream.cpp index 55993f5c5f..8e1215f5c8 100644 --- a/src/xrpld/app/tx/detail/OfferStream.cpp +++ b/src/xrpld/app/tx/detail/OfferStream.cpp @@ -19,9 +19,9 @@ #include #include -#include #include +#include #include #include diff --git a/src/xrpld/app/tx/detail/OfferStream.h b/src/xrpld/app/tx/detail/OfferStream.h index cf6c1c3d2d..6470f876a6 100644 --- a/src/xrpld/app/tx/detail/OfferStream.h +++ b/src/xrpld/app/tx/detail/OfferStream.h @@ -22,11 +22,11 @@ #include #include -#include #include #include #include +#include #include diff --git a/src/xrpld/app/tx/detail/PayChan.cpp b/src/xrpld/app/tx/detail/PayChan.cpp index d9e53ac75c..32c0abeb93 100644 --- a/src/xrpld/app/tx/detail/PayChan.cpp +++ b/src/xrpld/app/tx/detail/PayChan.cpp @@ -17,13 +17,13 @@ */ //============================================================================== -#include #include -#include -#include #include #include +#include +#include +#include #include #include #include @@ -171,15 +171,16 @@ PayChanCreate::makeTxConsequences(PreflightContext const& ctx) return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()}; } +std::uint32_t +PayChanCreate::getFlagsMask(PreflightContext const& ctx) +{ + // 0 means "Allow any flags" + return ctx.rules.enabled(fix1543) ? tfUniversalMask : 0; +} + NotTEC PayChanCreate::preflight(PreflightContext const& ctx) { - if (ctx.rules.enabled(fix1543) && ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - if (!isXRP(ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::zero)) return temBAD_AMOUNT; @@ -189,7 +190,7 @@ PayChanCreate::preflight(PreflightContext const& ctx) if (!publicKeyType(ctx.tx[sfPublicKey])) return temMALFORMED; - return preflight2(ctx); + return tesSUCCESS; } TER @@ -286,6 +287,10 @@ PayChanCreate::doApply() (*slep)[~sfCancelAfter] = ctx_.tx[~sfCancelAfter]; (*slep)[~sfSourceTag] = ctx_.tx[~sfSourceTag]; (*slep)[~sfDestinationTag] = ctx_.tx[~sfDestinationTag]; + if (ctx_.view().rules().enabled(fixIncludeKeyletFields)) + { + (*slep)[sfSequence] = ctx_.tx.getSeqValue(); + } ctx_.view().insert(slep); @@ -326,19 +331,20 @@ PayChanFund::makeTxConsequences(PreflightContext const& ctx) return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()}; } +std::uint32_t +PayChanFund::getFlagsMask(PreflightContext const& ctx) +{ + // 0 means "Allow any flags" + return ctx.rules.enabled(fix1543) ? tfUniversalMask : 0; +} + NotTEC PayChanFund::preflight(PreflightContext const& ctx) { - if (ctx.rules.enabled(fix1543) && ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - if (!isXRP(ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::zero)) return temBAD_AMOUNT; - return preflight2(ctx); + return tesSUCCESS; } TER @@ -416,16 +422,23 @@ PayChanFund::doApply() //------------------------------------------------------------------------------ +bool +PayChanClaim::checkExtraFeatures(PreflightContext const& ctx) +{ + return !ctx.tx.isFieldPresent(sfCredentialIDs) || + ctx.rules.enabled(featureCredentials); +} + +std::uint32_t +PayChanClaim::getFlagsMask(PreflightContext const& ctx) +{ + // 0 means "Allow any flags" + return ctx.rules.enabled(fix1543) ? tfPayChanClaimMask : 0; +} + NotTEC PayChanClaim::preflight(PreflightContext const& ctx) { - if (ctx.tx.isFieldPresent(sfCredentialIDs) && - !ctx.rules.enabled(featureCredentials)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - auto const bal = ctx.tx[~sfBalance]; if (bal && (!isXRP(*bal) || *bal <= beast::zero)) return temBAD_AMOUNT; @@ -440,9 +453,6 @@ PayChanClaim::preflight(PreflightContext const& ctx) { auto const flags = ctx.tx.getFlags(); - if (ctx.rules.enabled(fix1543) && (flags & tfPayChanClaimMask)) - return temINVALID_FLAG; - if ((flags & tfClose) && (flags & tfRenew)) return temMALFORMED; } @@ -477,7 +487,7 @@ PayChanClaim::preflight(PreflightContext const& ctx) !isTesSuccess(err)) return err; - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/PayChan.h b/src/xrpld/app/tx/detail/PayChan.h index 2e09c473dc..b25a4529be 100644 --- a/src/xrpld/app/tx/detail/PayChan.h +++ b/src/xrpld/app/tx/detail/PayChan.h @@ -36,6 +36,9 @@ public: static TxConsequences makeTxConsequences(PreflightContext const& ctx); + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); @@ -62,6 +65,9 @@ public: static TxConsequences makeTxConsequences(PreflightContext const& ctx); + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); @@ -82,6 +88,12 @@ public: { } + static bool + checkExtraFeatures(PreflightContext const& ctx); + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/Payment.cpp b/src/xrpld/app/tx/detail/Payment.cpp index 784330b203..8bc0e891d0 100644 --- a/src/xrpld/app/tx/detail/Payment.cpp +++ b/src/xrpld/app/tx/detail/Payment.cpp @@ -17,14 +17,14 @@ */ //============================================================================== -#include #include #include #include #include -#include #include +#include +#include #include #include #include @@ -65,20 +65,33 @@ getMaxSourceAmount( dstAmount < beast::zero); } -NotTEC -Payment::preflight(PreflightContext const& ctx) +bool +Payment::checkExtraFeatures(PreflightContext const& ctx) { if (ctx.tx.isFieldPresent(sfCredentialIDs) && !ctx.rules.enabled(featureCredentials)) - return temDISABLED; - + return false; if (ctx.tx.isFieldPresent(sfDomainID) && !ctx.rules.enabled(featurePermissionedDEX)) - return temDISABLED; + return false; - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; + return true; +} +std::uint32_t +Payment::getFlagsMask(PreflightContext const& ctx) +{ + auto& tx = ctx.tx; + + STAmount const dstAmount(tx.getFieldAmount(sfAmount)); + bool const mptDirect = dstAmount.holds(); + + return mptDirect ? tfMPTPaymentMask : tfPaymentMask; +} + +NotTEC +Payment::preflight(PreflightContext const& ctx) +{ auto& tx = ctx.tx; auto& j = ctx.j; @@ -90,14 +103,6 @@ Payment::preflight(PreflightContext const& ctx) std::uint32_t const txFlags = tx.getFlags(); - std::uint32_t paymentMask = mptDirect ? tfMPTPaymentMask : tfPaymentMask; - - if (txFlags & paymentMask) - { - JLOG(j.trace()) << "Malformed transaction: Invalid flags set."; - return temINVALID_FLAG; - } - if (mptDirect && ctx.tx.isFieldPresent(sfPaths)) return temMALFORMED; @@ -242,7 +247,7 @@ Payment::preflight(PreflightContext const& ctx) !isTesSuccess(err)) return err; - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/Payment.h b/src/xrpld/app/tx/detail/Payment.h index 010a2453cf..04bba390e2 100644 --- a/src/xrpld/app/tx/detail/Payment.h +++ b/src/xrpld/app/tx/detail/Payment.h @@ -42,6 +42,12 @@ public: static TxConsequences makeTxConsequences(PreflightContext const& ctx); + static bool + checkExtraFeatures(PreflightContext const& ctx); + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/PermissionedDomainDelete.cpp b/src/xrpld/app/tx/detail/PermissionedDomainDelete.cpp index 64c498b68b..9fe48ba515 100644 --- a/src/xrpld/app/tx/detail/PermissionedDomainDelete.cpp +++ b/src/xrpld/app/tx/detail/PermissionedDomainDelete.cpp @@ -18,8 +18,8 @@ //============================================================================== #include -#include +#include #include namespace ripple { @@ -27,23 +27,11 @@ namespace ripple { NotTEC PermissionedDomainDelete::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featurePermissionedDomains)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfUniversalMask) - { - JLOG(ctx.j.debug()) << "PermissionedDomainDelete: invalid flags."; - return temINVALID_FLAG; - } - auto const domain = ctx.tx.getFieldH256(sfDomainID); if (domain == beast::zero) return temMALFORMED; - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/PermissionedDomainSet.cpp b/src/xrpld/app/tx/detail/PermissionedDomainSet.cpp index 6e2df2a082..d9fa481bb6 100644 --- a/src/xrpld/app/tx/detail/PermissionedDomainSet.cpp +++ b/src/xrpld/app/tx/detail/PermissionedDomainSet.cpp @@ -17,10 +17,10 @@ */ //============================================================================== -#include #include -#include +#include +#include #include #include @@ -28,22 +28,15 @@ namespace ripple { +bool +PermissionedDomainSet::checkExtraFeatures(PreflightContext const& ctx) +{ + return ctx.rules.enabled(featureCredentials); +} + NotTEC PermissionedDomainSet::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featurePermissionedDomains) || - !ctx.rules.enabled(featureCredentials)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfUniversalMask) - { - JLOG(ctx.j.debug()) << "PermissionedDomainSet: invalid flags."; - return temINVALID_FLAG; - } - if (auto err = credentials::checkArray( ctx.tx.getFieldArray(sfAcceptedCredentials), maxPermissionedDomainCredentialsArraySize, @@ -55,7 +48,7 @@ PermissionedDomainSet::preflight(PreflightContext const& ctx) if (domain && *domain == beast::zero) return temMALFORMED; - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/PermissionedDomainSet.h b/src/xrpld/app/tx/detail/PermissionedDomainSet.h index 502d576e32..ed27896a3b 100644 --- a/src/xrpld/app/tx/detail/PermissionedDomainSet.h +++ b/src/xrpld/app/tx/detail/PermissionedDomainSet.h @@ -33,6 +33,9 @@ public: { } + static bool + checkExtraFeatures(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/SetAccount.cpp b/src/xrpld/app/tx/detail/SetAccount.cpp index ec618981c1..c2129ba1e1 100644 --- a/src/xrpld/app/tx/detail/SetAccount.cpp +++ b/src/xrpld/app/tx/detail/SetAccount.cpp @@ -20,9 +20,9 @@ #include #include #include -#include #include +#include #include #include #include @@ -57,23 +57,20 @@ SetAccount::makeTxConsequences(PreflightContext const& ctx) return TxConsequences{ctx.tx, getTxConsequencesCategory(ctx.tx)}; } +std::uint32_t +SetAccount::getFlagsMask(PreflightContext const& ctx) +{ + return tfAccountSetMask; +} + NotTEC SetAccount::preflight(PreflightContext const& ctx) { - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - auto& tx = ctx.tx; auto& j = ctx.j; std::uint32_t const uTxFlags = tx.getFlags(); - if (uTxFlags & tfAccountSetMask) - { - JLOG(j.trace()) << "Malformed transaction: Invalid flags set."; - return temINVALID_FLAG; - } - std::uint32_t const uSetFlag = tx.getFieldU32(sfSetFlag); std::uint32_t const uClearFlag = tx.getFieldU32(sfClearFlag); @@ -186,7 +183,7 @@ SetAccount::preflight(PreflightContext const& ctx) return temMALFORMED; } - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/SetAccount.h b/src/xrpld/app/tx/detail/SetAccount.h index ed4242c250..bcc0a61b1b 100644 --- a/src/xrpld/app/tx/detail/SetAccount.h +++ b/src/xrpld/app/tx/detail/SetAccount.h @@ -38,6 +38,9 @@ public: static TxConsequences makeTxConsequences(PreflightContext const& ctx); + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/SetOracle.cpp b/src/xrpld/app/tx/detail/SetOracle.cpp index d598507cb7..81f20b4342 100644 --- a/src/xrpld/app/tx/detail/SetOracle.cpp +++ b/src/xrpld/app/tx/detail/SetOracle.cpp @@ -18,9 +18,9 @@ //============================================================================== #include -#include -#include +#include +#include #include #include #include @@ -39,15 +39,6 @@ tokenPairKey(STObject const& pair) NotTEC SetOracle::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featurePriceOracle)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; - auto const& dataSeries = ctx.tx.getFieldArray(sfPriceDataSeries); if (dataSeries.empty()) return temARRAY_EMPTY; @@ -64,7 +55,7 @@ SetOracle::preflight(PreflightContext const& ctx) isInvalidLength(sfAssetClass, maxOracleSymbolClass)) return temMALFORMED; - return preflight2(ctx); + return tesSUCCESS; } TER @@ -271,6 +262,11 @@ SetOracle::doApply() if (ctx_.tx.isFieldPresent(sfURI)) sle->setFieldVL(sfURI, ctx_.tx[sfURI]); sle->setFieldU32(sfLastUpdateTime, ctx_.tx[sfLastUpdateTime]); + if (!sle->isFieldPresent(sfOracleDocumentID) && + ctx_.view().rules().enabled(fixIncludeKeyletFields)) + { + (*sle)[sfOracleDocumentID] = ctx_.tx[sfOracleDocumentID]; + } auto const newCount = pairs.size() > 5 ? 2 : 1; auto const adjust = newCount - oldCount; @@ -285,6 +281,10 @@ SetOracle::doApply() sle = std::make_shared(oracleID); sle->setAccountID(sfOwner, ctx_.tx.getAccountID(sfAccount)); + if (ctx_.view().rules().enabled(fixIncludeKeyletFields)) + { + (*sle)[sfOracleDocumentID] = ctx_.tx[sfOracleDocumentID]; + } sle->setFieldVL(sfProvider, ctx_.tx[sfProvider]); if (ctx_.tx.isFieldPresent(sfURI)) sle->setFieldVL(sfURI, ctx_.tx[sfURI]); diff --git a/src/xrpld/app/tx/detail/SetRegularKey.cpp b/src/xrpld/app/tx/detail/SetRegularKey.cpp index 92d130a15a..4e063e7d1f 100644 --- a/src/xrpld/app/tx/detail/SetRegularKey.cpp +++ b/src/xrpld/app/tx/detail/SetRegularKey.cpp @@ -51,18 +51,6 @@ SetRegularKey::calculateBaseFee(ReadView const& view, STTx const& tx) NotTEC SetRegularKey::preflight(PreflightContext const& ctx) { - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - std::uint32_t const uTxFlags = ctx.tx.getFlags(); - - if (uTxFlags & tfUniversalMask) - { - JLOG(ctx.j.trace()) << "Malformed transaction: Invalid flags set."; - - return temINVALID_FLAG; - } - if (ctx.rules.enabled(fixMasterKeyAsRegularKey) && ctx.tx.isFieldPresent(sfRegularKey) && (ctx.tx.getAccountID(sfRegularKey) == ctx.tx.getAccountID(sfAccount))) @@ -70,7 +58,7 @@ SetRegularKey::preflight(PreflightContext const& ctx) return temBAD_REGKEY; } - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/SetSignerList.cpp b/src/xrpld/app/tx/detail/SetSignerList.cpp index 4a1ee703a0..b5d9d4d5b8 100644 --- a/src/xrpld/app/tx/detail/SetSignerList.cpp +++ b/src/xrpld/app/tx/detail/SetSignerList.cpp @@ -19,9 +19,9 @@ #include #include -#include #include +#include #include #include #include @@ -37,7 +37,7 @@ namespace ripple { // We're prepared for there to be multiple signer lists in the future, // but we don't need them yet. So for the time being we're manually // setting the sfSignerListID to zero in all cases. -static std::uint32_t const defaultSignerListID_ = 0; +static std::uint32_t const DEFAULT_SIGNER_LIST_ID = 0; std::tuple< NotTEC, @@ -77,19 +77,16 @@ SetSignerList::determineOperation( return std::make_tuple(tesSUCCESS, quorum, sign, op); } +std::uint32_t +SetSignerList::getFlagsMask(PreflightContext const& ctx) +{ + // 0 means "Allow any flags" + return ctx.rules.enabled(fixInvalidTxFlags) ? tfUniversalMask : 0; +} + NotTEC SetSignerList::preflight(PreflightContext const& ctx) { - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.rules.enabled(fixInvalidTxFlags) && - (ctx.tx.getFlags() & tfUniversalMask)) - { - JLOG(ctx.j.debug()) << "SetSignerList: invalid flags."; - return temINVALID_FLAG; - } - auto const result = determineOperation(ctx.tx, ctx.flags, ctx.j); if (std::get<0>(result) != tesSUCCESS) @@ -119,7 +116,7 @@ SetSignerList::preflight(PreflightContext const& ctx) } } - return preflight2(ctx); + return tesSUCCESS; } TER @@ -424,8 +421,12 @@ SetSignerList::writeSignersToSLE( std::uint32_t flags) const { // Assign the quorum, default SignerListID, and flags. + if (ctx_.view().rules().enabled(fixIncludeKeyletFields)) + { + ledgerEntry->setAccountID(sfOwner, account_); + } ledgerEntry->setFieldU32(sfSignerQuorum, quorum_); - ledgerEntry->setFieldU32(sfSignerListID, defaultSignerListID_); + ledgerEntry->setFieldU32(sfSignerListID, DEFAULT_SIGNER_LIST_ID); if (flags) // Only set flags if they are non-default (default is zero). ledgerEntry->setFieldU32(sfFlags, flags); diff --git a/src/xrpld/app/tx/detail/SetSignerList.h b/src/xrpld/app/tx/detail/SetSignerList.h index 1827aca975..be2df8152e 100644 --- a/src/xrpld/app/tx/detail/SetSignerList.h +++ b/src/xrpld/app/tx/detail/SetSignerList.h @@ -51,6 +51,9 @@ public: { } + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/SetTrust.cpp b/src/xrpld/app/tx/detail/SetTrust.cpp index d3b39aaf11..21d4534f93 100644 --- a/src/xrpld/app/tx/detail/SetTrust.cpp +++ b/src/xrpld/app/tx/detail/SetTrust.cpp @@ -19,9 +19,9 @@ #include #include -#include #include +#include #include #include #include @@ -67,23 +67,20 @@ computeFreezeFlags( namespace ripple { +std::uint32_t +SetTrust::getFlagsMask(PreflightContext const& ctx) +{ + return tfTrustSetMask; +} + NotTEC SetTrust::preflight(PreflightContext const& ctx) { - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - auto& tx = ctx.tx; auto& j = ctx.j; std::uint32_t const uTxFlags = tx.getFlags(); - if (uTxFlags & tfTrustSetMask) - { - JLOG(j.trace()) << "Malformed transaction: Invalid flags set."; - return temINVALID_FLAG; - } - if (!ctx.rules.enabled(featureDeepFreeze)) { // Even though the deep freeze flags are included in the @@ -127,7 +124,7 @@ SetTrust::preflight(PreflightContext const& ctx) return temDST_NEEDED; } - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/SetTrust.h b/src/xrpld/app/tx/detail/SetTrust.h index a0476918ac..443080bf74 100644 --- a/src/xrpld/app/tx/detail/SetTrust.h +++ b/src/xrpld/app/tx/detail/SetTrust.h @@ -35,6 +35,9 @@ public: { } + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/Taker.h b/src/xrpld/app/tx/detail/Taker.h new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index 0db0484842..f6b8b3c9d2 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -18,7 +18,6 @@ //============================================================================== #include -#include #include #include #include @@ -26,11 +25,12 @@ #include #include #include -#include #include #include #include +#include +#include #include #include #include @@ -41,7 +41,7 @@ namespace ripple { /** Performs early sanity checks on the txid */ NotTEC -preflight0(PreflightContext const& ctx) +preflight0(PreflightContext const& ctx, std::uint32_t flagMask) { if (isPseudoTx(ctx.tx) && ctx.tx.isFlag(tfInnerBatchTxn)) { @@ -83,12 +83,84 @@ preflight0(PreflightContext const& ctx) return temINVALID; } + if (ctx.tx.getFlags() & flagMask) + { + JLOG(ctx.j.debug()) + << ctx.tx.peekAtField(sfTransactionType).getFullText() + << ": invalid flags."; + return temINVALID_FLAG; + } + return tesSUCCESS; } +namespace detail { + +/** Checks the validity of the transactor signing key. + * + * Normally called from preflight1. + */ +NotTEC +preflightCheckSigningKey(STObject const& sigObject, beast::Journal j) +{ + if (auto const spk = sigObject.getFieldVL(sfSigningPubKey); + !spk.empty() && !publicKeyType(makeSlice(spk))) + { + JLOG(j.debug()) << "preflightCheckSigningKey: invalid signing key"; + return temBAD_SIGNATURE; + } + return tesSUCCESS; +} + +std::optional +preflightCheckSimulateKeys( + ApplyFlags flags, + STObject const& sigObject, + beast::Journal j) +{ + if (flags & tapDRY_RUN) // simulation + { + std::optional const signature = sigObject[~sfTxnSignature]; + if (signature && !signature->empty()) + { + // NOTE: This code should never be hit because it's checked in the + // `simulate` RPC + return temINVALID; // LCOV_EXCL_LINE + } + + if (!sigObject.isFieldPresent(sfSigners)) + { + // no signers, no signature - a valid simulation + return tesSUCCESS; + } + + for (auto const& signer : sigObject.getFieldArray(sfSigners)) + { + if (signer.isFieldPresent(sfTxnSignature) && + !signer[sfTxnSignature].empty()) + { + // NOTE: This code should never be hit because it's + // checked in the `simulate` RPC + return temINVALID; // LCOV_EXCL_LINE + } + } + + Slice const signingPubKey = sigObject[sfSigningPubKey]; + if (!signingPubKey.empty()) + { + // trying to single-sign _and_ multi-sign a transaction + return temINVALID; + } + return tesSUCCESS; + } + return {}; +} + +} // namespace detail + /** Performs early sanity checks on the account and fee fields */ NotTEC -preflight1(PreflightContext const& ctx) +Transactor::preflight1(PreflightContext const& ctx, std::uint32_t flagMask) { // This is inappropriate in preflight0, because only Change transactions // skip this function, and those do not allow an sfTicketSequence field. @@ -107,8 +179,7 @@ preflight1(PreflightContext const& ctx) return temBAD_SIGNER; } - auto const ret = preflight0(ctx); - if (!isTesSuccess(ret)) + if (auto const ret = preflight0(ctx, flagMask)) return ret; auto const id = ctx.tx.getAccountID(sfAccount); @@ -126,13 +197,8 @@ preflight1(PreflightContext const& ctx) return temBAD_FEE; } - auto const spk = ctx.tx.getSigningPubKey(); - - if (!spk.empty() && !publicKeyType(makeSlice(spk))) - { - JLOG(ctx.j.debug()) << "preflight1: invalid signing key"; - return temBAD_SIGNATURE; - } + if (auto const ret = detail::preflightCheckSigningKey(ctx.tx, ctx.j)) + return ret; // An AccountTxnID field constrains transaction ordering more than the // Sequence field. Tickets, on the other hand, reduce ordering @@ -157,41 +223,13 @@ preflight1(PreflightContext const& ctx) /** Checks whether the signature appears valid */ NotTEC -preflight2(PreflightContext const& ctx) +Transactor::preflight2(PreflightContext const& ctx) { - if (ctx.flags & tapDRY_RUN) // simulation - { - if (!ctx.tx.getSignature().empty()) - { - // NOTE: This code should never be hit because it's checked in the - // `simulate` RPC - return temINVALID; // LCOV_EXCL_LINE - } - - if (!ctx.tx.isFieldPresent(sfSigners)) - { - // no signers, no signature - a valid simulation - return tesSUCCESS; - } - - for (auto const& signer : ctx.tx.getFieldArray(sfSigners)) - { - if (signer.isFieldPresent(sfTxnSignature) && - !signer[sfTxnSignature].empty()) - { - // NOTE: This code should never be hit because it's - // checked in the `simulate` RPC - return temINVALID; // LCOV_EXCL_LINE - } - } - - if (!ctx.tx.getSigningPubKey().empty()) - { - // trying to single-sign _and_ multi-sign a transaction - return temINVALID; - } - return tesSUCCESS; - } + if (auto const ret = + detail::preflightCheckSimulateKeys(ctx.flags, ctx.tx, ctx.j)) + // Skips following checks if the transaction is being simulated, + // regardless of success or failure + return *ret; auto const sigValid = checkValidity( ctx.app.getHashRouter(), ctx.tx, ctx.rules, ctx.app.config()); @@ -206,10 +244,35 @@ preflight2(PreflightContext const& ctx) //------------------------------------------------------------------------------ Transactor::Transactor(ApplyContext& ctx) - : ctx_(ctx), j_(ctx.journal), account_(ctx.tx.getAccountID(sfAccount)) + : ctx_(ctx) + , sink_(ctx.journal, to_short_string(ctx.tx.getTransactionID()) + " ") + , j_(sink_) + , account_(ctx.tx.getAccountID(sfAccount)) { } +bool +Transactor::validDataLength( + std::optional const& slice, + std::size_t maxLength) +{ + if (!slice) + return true; + return !slice->empty() && slice->length() <= maxLength; +} + +std::uint32_t +Transactor::getFlagsMask(PreflightContext const& ctx) +{ + return tfUniversalMask; +} + +NotTEC +Transactor::preflightSigValidated(PreflightContext const& ctx) +{ + return tesSUCCESS; +} + TER Transactor::checkPermission(ReadView const& view, STTx const& tx) { @@ -244,6 +307,27 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx) return baseFee + (signerCount * baseFee); } +// Returns the fee in fee units, not scaled for load. +XRPAmount +Transactor::calculateOwnerReserveFee(ReadView const& view, STTx const& tx) +{ + // Assumption: One reserve increment is typically much greater than one base + // fee. + // This check is in an assert so that it will come to the attention of + // developers if that assumption is not correct. If the owner reserve is not + // significantly larger than the base fee (or even worse, smaller), we will + // need to rethink charging an owner reserve as a transaction fee. + // TODO: This function is static, and I don't want to add more parameters. + // When it is finally refactored to be in a context that has access to the + // Application, include "app().overlay().networkID() > 2 ||" in the + // condition. + XRPL_ASSERT( + view.fees().increment > view.fees().base * 100, + "ripple::Transactor::calculateOwnerReserveFee : Owner reserve is " + "reasonable"); + return view.fees().increment; +} + XRPAmount Transactor::minimumFee( Application& app, @@ -575,60 +659,69 @@ Transactor::apply() } NotTEC -Transactor::checkSign(PreclaimContext const& ctx) +Transactor::checkSign( + ReadView const& view, + ApplyFlags flags, + AccountID const& idAccount, + STObject const& sigObject, + beast::Journal const j) { - auto const pkSigner = ctx.tx.getSigningPubKey(); + auto const pkSigner = sigObject.getFieldVL(sfSigningPubKey); // Ignore signature check on batch inner transactions - if (ctx.tx.isFlag(tfInnerBatchTxn) && - ctx.view.rules().enabled(featureBatch)) + if (sigObject.isFlag(tfInnerBatchTxn) && view.rules().enabled(featureBatch)) { // Defensive Check: These values are also checked in Batch::preflight - if (ctx.tx.isFieldPresent(sfTxnSignature) || !pkSigner.empty() || - ctx.tx.isFieldPresent(sfSigners)) + if (sigObject.isFieldPresent(sfTxnSignature) || !pkSigner.empty() || + sigObject.isFieldPresent(sfSigners)) { return temINVALID_FLAG; // LCOV_EXCL_LINE } return tesSUCCESS; } - if ((ctx.flags & tapDRY_RUN) && pkSigner.empty() && - !ctx.tx.isFieldPresent(sfSigners)) + if ((flags & tapDRY_RUN) && pkSigner.empty() && + !sigObject.isFieldPresent(sfSigners)) { // simulate: skip signature validation when neither SigningPubKey nor // Signers are provided return tesSUCCESS; } - auto const idAccount = ctx.tx[~sfDelegate].value_or(ctx.tx[sfAccount]); - // If the pk is empty and not simulate or simulate and signers, // then we must be multi-signing. - if (ctx.tx.isFieldPresent(sfSigners)) + if (sigObject.isFieldPresent(sfSigners)) { - STArray const& txSigners(ctx.tx.getFieldArray(sfSigners)); - return checkMultiSign(ctx.view, idAccount, txSigners, ctx.flags, ctx.j); + return checkMultiSign(view, flags, idAccount, sigObject, j); } // Check Single Sign XRPL_ASSERT( - !pkSigner.empty(), - "ripple::Transactor::checkSingleSign : non-empty signer or simulation"); + !pkSigner.empty(), "ripple::Transactor::checkSign : non-empty signer"); if (!publicKeyType(makeSlice(pkSigner))) { - JLOG(ctx.j.trace()) - << "checkSingleSign: signing public key type is unknown"; + JLOG(j.trace()) << "checkSign: signing public key type is unknown"; return tefBAD_AUTH; // FIXME: should be better error! } + + // Look up the account. auto const idSigner = pkSigner.empty() ? idAccount : calcAccountID(PublicKey(makeSlice(pkSigner))); - auto const sleAccount = ctx.view.read(keylet::account(idAccount)); + auto const sleAccount = view.read(keylet::account(idAccount)); if (!sleAccount) return terNO_ACCOUNT; - return checkSingleSign( - idSigner, idAccount, sleAccount, ctx.view.rules(), ctx.j); + return checkSingleSign(view, idSigner, idAccount, sleAccount, j); +} + +NotTEC +Transactor::checkSign(PreclaimContext const& ctx) +{ + auto const idAccount = ctx.tx.isFieldPresent(sfDelegate) + ? ctx.tx.getAccountID(sfDelegate) + : ctx.tx.getAccountID(sfAccount); + return checkSign(ctx.view, ctx.flags, idAccount, ctx.tx, ctx.j); } NotTEC @@ -643,9 +736,8 @@ Transactor::checkBatchSign(PreclaimContext const& ctx) Blob const& pkSigner = signer.getFieldVL(sfSigningPubKey); if (pkSigner.empty()) { - STArray const& txSigners(signer.getFieldArray(sfSigners)); if (ret = checkMultiSign( - ctx.view, idAccount, txSigners, ctx.flags, ctx.j); + ctx.view, ctx.flags, idAccount, signer, ctx.j); !isTesSuccess(ret)) return ret; } @@ -670,7 +762,7 @@ Transactor::checkBatchSign(PreclaimContext const& ctx) } if (ret = checkSingleSign( - idSigner, idAccount, sleAccount, ctx.view.rules(), ctx.j); + ctx.view, idSigner, idAccount, sleAccount, ctx.j); !isTesSuccess(ret)) return ret; } @@ -680,15 +772,15 @@ Transactor::checkBatchSign(PreclaimContext const& ctx) NotTEC Transactor::checkSingleSign( + ReadView const& view, AccountID const& idSigner, AccountID const& idAccount, std::shared_ptr sleAccount, - Rules const& rules, - beast::Journal j) + beast::Journal const j) { bool const isMasterDisabled = sleAccount->isFlag(lsfDisableMaster); - if (rules.enabled(fixMasterKeyAsRegularKey)) + if (view.rules().enabled(fixMasterKeyAsRegularKey)) { // Signed with regular key. if ((*sleAccount)[~sfRegularKey] == idSigner) @@ -742,12 +834,12 @@ Transactor::checkSingleSign( NotTEC Transactor::checkMultiSign( ReadView const& view, + ApplyFlags flags, AccountID const& id, - STArray const& txSigners, - ApplyFlags const& flags, - beast::Journal j) + STObject const& sigObject, + beast::Journal const j) { - // Get mTxnAccountID's SignerList and Quorum. + // Get id's SignerList and Quorum. std::shared_ptr sleAccountSigners = view.read(keylet::signers(id)); // If the signer list doesn't exist the account is not multi-signing. @@ -773,6 +865,7 @@ Transactor::checkMultiSign( return accountSigners.error(); // Get the array of transaction signers. + STArray const& txSigners(sigObject.getFieldArray(sfSigners)); // Walk the accountSigners performing a variety of checks and see if // the quorum is met. diff --git a/src/xrpld/app/tx/detail/Transactor.h b/src/xrpld/app/tx/detail/Transactor.h index 42d4861a63..17ef62e607 100644 --- a/src/xrpld/app/tx/detail/Transactor.h +++ b/src/xrpld/app/tx/detail/Transactor.h @@ -24,6 +24,7 @@ #include #include +#include #include #include @@ -133,11 +134,14 @@ public: class TxConsequences; struct PreflightResult; +// Needed for preflight specialization +class Change; class Transactor { protected: ApplyContext& ctx_; + beast::WrappedSink sink_; beast::Journal const j_; AccountID const account_; @@ -196,6 +200,35 @@ public: static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); + /* Do NOT define an invokePreflight function in a derived class. + Instead, define: + + // Optional if the transaction is gated on an amendment that + // isn't specified in transactions.macro + static bool + checkExtraFeatures(PreflightContext const& ctx); + + // Optional if the transaction uses any flags other than tfUniversal + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + + // Required, even if it just returns tesSUCCESS. + static NotTEC + preflight(PreflightContext const& ctx); + + // Optional, rarely needed, if the transaction does any expensive + // checks after the signature is verified. + static NotTEC preflightSigValidated(PreflightContext const& ctx); + + * Do not try to call preflight1 or preflight2 directly. + * Do not check whether relevant amendments are enabled in preflight. + Instead, define checkExtraFeatures. + * Do not check flags in preflight. Instead, define getFlagsMask. + */ + template + static NotTEC + invokePreflight(PreflightContext const& ctx); + static TER preclaim(PreclaimContext const& ctx) { @@ -244,6 +277,44 @@ protected: Fees const& fees, ApplyFlags flags); + // Returns the fee in fee units, not scaled for load. + static XRPAmount + calculateOwnerReserveFee(ReadView const& view, STTx const& tx); + + static NotTEC + checkSign( + ReadView const& view, + ApplyFlags flags, + AccountID const& idAccount, + STObject const& sigObject, + beast::Journal const j); + + // Base class always returns true + static bool + checkExtraFeatures(PreflightContext const& ctx); + + // Base class always returns tfUniversalMask + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + + // Base class always returns tesSUCCESS + static NotTEC + preflightSigValidated(PreflightContext const& ctx); + + static bool + validDataLength(std::optional const& slice, std::size_t maxLength); + + template + static bool + validNumericRange(std::optional value, T max, T min = {}); + + template + static bool + validNumericRange( + std::optional value, + unit::ValueUnit max, + unit::ValueUnit min = {}); + private: std::pair reset(XRPAmount fee); @@ -254,33 +325,120 @@ private: payFee(); static NotTEC checkSingleSign( + ReadView const& view, AccountID const& idSigner, AccountID const& idAccount, std::shared_ptr sleAccount, - Rules const& rules, - beast::Journal j); + beast::Journal const j); static NotTEC checkMultiSign( ReadView const& view, - AccountID const& idAccount, - STArray const& txSigners, - ApplyFlags const& flags, - beast::Journal j); + ApplyFlags flags, + AccountID const& id, + STObject const& sigObject, + beast::Journal const j); void trapTransaction(uint256) const; + + /** Performs early sanity checks on the account and fee fields. + + (And passes flagMask to preflight0) + + Do not try to call preflight1 from preflight() in derived classes. See + the description of invokePreflight for details. + */ + static NotTEC + preflight1(PreflightContext const& ctx, std::uint32_t flagMask); + + /** Checks whether the signature appears valid + + Do not try to call preflight2 from preflight() in derived classes. See + the description of invokePreflight for details. + */ + static NotTEC + preflight2(PreflightContext const& ctx); }; -/** Performs early sanity checks on the txid */ -NotTEC -preflight0(PreflightContext const& ctx); +inline bool +Transactor::checkExtraFeatures(PreflightContext const& ctx) +{ + return true; +} -/** Performs early sanity checks on the account and fee fields */ +/** Performs early sanity checks on the txid and flags */ NotTEC -preflight1(PreflightContext const& ctx); +preflight0(PreflightContext const& ctx, std::uint32_t flagMask); -/** Checks whether the signature appears valid */ +namespace detail { + +/** Checks the validity of the transactor signing key. + * + * Normally called from preflight1 with ctx.tx. + */ NotTEC -preflight2(PreflightContext const& ctx); +preflightCheckSigningKey(STObject const& sigObject, beast::Journal j); + +/** Checks the special signing key state needed for simulation + * + * Normally called from preflight2 with ctx.tx. + */ +std::optional +preflightCheckSimulateKeys( + ApplyFlags flags, + STObject const& sigObject, + beast::Journal j); +} // namespace detail + +// Defined in Change.cpp +template <> +NotTEC +Transactor::invokePreflight(PreflightContext const& ctx); + +template +NotTEC +Transactor::invokePreflight(PreflightContext const& ctx) +{ + // Using this lookup does NOT require checking the fixDelegateV1_1. The data + // exists regardless of whether it is enabled. + auto const feature = + Permission::getInstance().getTxFeature(ctx.tx.getTxnType()); + + if (feature && !ctx.rules.enabled(*feature)) + return temDISABLED; + + if (!T::checkExtraFeatures(ctx)) + return temDISABLED; + + if (auto const ret = preflight1(ctx, T::getFlagsMask(ctx))) + return ret; + + if (auto const ret = T::preflight(ctx)) + return ret; + + if (auto const ret = preflight2(ctx)) + return ret; + + return T::preflightSigValidated(ctx); +} + +template +bool +Transactor::validNumericRange(std::optional value, T max, T min) +{ + if (!value) + return true; + return value >= min && value <= max; +} + +template +bool +Transactor::validNumericRange( + std::optional value, + unit::ValueUnit max, + unit::ValueUnit min) +{ + return validNumericRange(value, max.value(), min.value()); +} } // namespace ripple diff --git a/src/xrpld/app/tx/detail/VaultClawback.cpp b/src/xrpld/app/tx/detail/VaultClawback.cpp index 87740da179..45a56a6292 100644 --- a/src/xrpld/app/tx/detail/VaultClawback.cpp +++ b/src/xrpld/app/tx/detail/VaultClawback.cpp @@ -18,9 +18,9 @@ //============================================================================== #include -#include #include +#include #include #include #include @@ -35,15 +35,6 @@ namespace ripple { NotTEC VaultClawback::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureSingleAssetVault)) - return temDISABLED; - - if (auto const ter = preflight1(ctx)) - return ter; - - if (ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; - if (ctx.tx[sfVaultID] == beast::zero) { JLOG(ctx.j.debug()) << "VaultClawback: zero/empty vault ID."; @@ -78,7 +69,7 @@ VaultClawback::preflight(PreflightContext const& ctx) } } - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/VaultCreate.cpp b/src/xrpld/app/tx/detail/VaultCreate.cpp index 0b5cdd4fc0..9447976a32 100644 --- a/src/xrpld/app/tx/detail/VaultCreate.cpp +++ b/src/xrpld/app/tx/detail/VaultCreate.cpp @@ -20,8 +20,8 @@ #include #include #include -#include +#include #include #include #include @@ -35,28 +35,27 @@ namespace ripple { +bool +VaultCreate::checkExtraFeatures(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureMPTokensV1)) + return false; + + return !ctx.tx.isFieldPresent(sfDomainID) || + ctx.rules.enabled(featurePermissionedDomains); +} + +std::uint32_t +VaultCreate::getFlagsMask(PreflightContext const& ctx) +{ + return tfVaultCreateMask; +} + NotTEC VaultCreate::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureSingleAssetVault) || - !ctx.rules.enabled(featureMPTokensV1)) - return temDISABLED; - - if (ctx.tx.isFieldPresent(sfDomainID) && - !ctx.rules.enabled(featurePermissionedDomains)) - return temDISABLED; - - if (auto const ter = preflight1(ctx)) - return ter; - - if (ctx.tx.getFlags() & tfVaultCreateMask) - return temINVALID_FLAG; - - if (auto const data = ctx.tx[~sfData]) - { - if (data->empty() || data->length() > maxDataPayloadLength) - return temMALFORMED; - } + if (!validDataLength(ctx.tx[~sfData], maxDataPayloadLength)) + return temMALFORMED; if (auto const withdrawalPolicy = ctx.tx[~sfWithdrawalPolicy]) { @@ -96,14 +95,14 @@ VaultCreate::preflight(PreflightContext const& ctx) return temMALFORMED; } - return preflight2(ctx); + return tesSUCCESS; } XRPAmount VaultCreate::calculateBaseFee(ReadView const& view, STTx const& tx) { // One reserve increment is typically much greater than one base fee. - return view.fees().increment; + return calculateOwnerReserveFee(view, tx); } TER @@ -112,32 +111,8 @@ VaultCreate::preclaim(PreclaimContext const& ctx) auto const vaultAsset = ctx.tx[sfAsset]; auto const account = ctx.tx[sfAccount]; - if (vaultAsset.native()) - ; // No special checks for XRP - else if (vaultAsset.holds()) - { - auto mptID = vaultAsset.get().getMptID(); - auto issuance = ctx.view.read(keylet::mptIssuance(mptID)); - if (!issuance) - return tecOBJECT_NOT_FOUND; - if (!issuance->isFlag(lsfMPTCanTransfer)) - { - // NOTE: flag lsfMPTCanTransfer is immutable, so this is debug in - // VaultCreate only; in other vault function it's an error. - JLOG(ctx.j.debug()) - << "VaultCreate: vault assets are non-transferable."; - return tecNO_AUTH; - } - } - else if (vaultAsset.holds()) - { - auto const issuer = - ctx.view.read(keylet::account(vaultAsset.getIssuer())); - if (!issuer) - return terNO_ACCOUNT; - else if (!issuer->isFlag(lsfDefaultRipple)) - return terNO_RIPPLE; - } + if (auto const ter = canAddHolding(ctx.view, vaultAsset)) + return ter; // Check for pseudo-account issuers - we do not want a vault to hold such // assets (e.g. MPT shares to other vaults or AMM LPTokens) as they would be diff --git a/src/xrpld/app/tx/detail/VaultCreate.h b/src/xrpld/app/tx/detail/VaultCreate.h index 5555644629..3f952d540a 100644 --- a/src/xrpld/app/tx/detail/VaultCreate.h +++ b/src/xrpld/app/tx/detail/VaultCreate.h @@ -33,6 +33,12 @@ public: { } + static bool + checkExtraFeatures(PreflightContext const& ctx); + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/VaultDelete.cpp b/src/xrpld/app/tx/detail/VaultDelete.cpp index d4b74ae1d5..ab7db78956 100644 --- a/src/xrpld/app/tx/detail/VaultDelete.cpp +++ b/src/xrpld/app/tx/detail/VaultDelete.cpp @@ -18,8 +18,8 @@ //============================================================================== #include -#include +#include #include #include #include @@ -31,22 +31,13 @@ namespace ripple { NotTEC VaultDelete::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureSingleAssetVault)) - return temDISABLED; - - if (auto const ter = preflight1(ctx)) - return ter; - - if (ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; - if (ctx.tx[sfVaultID] == beast::zero) { JLOG(ctx.j.debug()) << "VaultDelete: zero/empty vault ID."; return temMALFORMED; } - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/VaultDeposit.cpp b/src/xrpld/app/tx/detail/VaultDeposit.cpp index 5cdcb43e20..75cf81b0b0 100644 --- a/src/xrpld/app/tx/detail/VaultDeposit.cpp +++ b/src/xrpld/app/tx/detail/VaultDeposit.cpp @@ -17,11 +17,11 @@ */ //============================================================================== -#include #include #include -#include +#include +#include #include #include #include @@ -36,15 +36,6 @@ namespace ripple { NotTEC VaultDeposit::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureSingleAssetVault)) - return temDISABLED; - - if (auto const ter = preflight1(ctx)) - return ter; - - if (ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; - if (ctx.tx[sfVaultID] == beast::zero) { JLOG(ctx.j.debug()) << "VaultDeposit: zero/empty vault ID."; @@ -54,7 +45,7 @@ VaultDeposit::preflight(PreflightContext const& ctx) if (ctx.tx[sfAmount] <= beast::zero) return temBAD_AMOUNT; - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/VaultSet.cpp b/src/xrpld/app/tx/detail/VaultSet.cpp index 4750f89be2..6057e40cfa 100644 --- a/src/xrpld/app/tx/detail/VaultSet.cpp +++ b/src/xrpld/app/tx/detail/VaultSet.cpp @@ -18,8 +18,8 @@ //============================================================================== #include -#include +#include #include #include #include @@ -30,28 +30,22 @@ namespace ripple { +bool +VaultSet::checkExtraFeatures(PreflightContext const& ctx) +{ + return !ctx.tx.isFieldPresent(sfDomainID) || + ctx.rules.enabled(featurePermissionedDomains); +} + NotTEC VaultSet::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureSingleAssetVault)) - return temDISABLED; - - if (ctx.tx.isFieldPresent(sfDomainID) && - !ctx.rules.enabled(featurePermissionedDomains)) - return temDISABLED; - - if (auto const ter = preflight1(ctx)) - return ter; - if (ctx.tx[sfVaultID] == beast::zero) { JLOG(ctx.j.debug()) << "VaultSet: zero/empty vault ID."; return temMALFORMED; } - if (ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; - if (auto const data = ctx.tx[~sfData]) { if (data->empty() || data->length() > maxDataPayloadLength) @@ -78,7 +72,7 @@ VaultSet::preflight(PreflightContext const& ctx) return temMALFORMED; } - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/VaultSet.h b/src/xrpld/app/tx/detail/VaultSet.h index f16aa6c284..b3eecbbab5 100644 --- a/src/xrpld/app/tx/detail/VaultSet.h +++ b/src/xrpld/app/tx/detail/VaultSet.h @@ -33,6 +33,9 @@ public: { } + static bool + checkExtraFeatures(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/VaultWithdraw.cpp b/src/xrpld/app/tx/detail/VaultWithdraw.cpp index 0ceaabbfde..509b795058 100644 --- a/src/xrpld/app/tx/detail/VaultWithdraw.cpp +++ b/src/xrpld/app/tx/detail/VaultWithdraw.cpp @@ -17,10 +17,10 @@ */ //============================================================================== -#include #include -#include +#include +#include #include #include #include @@ -33,15 +33,6 @@ namespace ripple { NotTEC VaultWithdraw::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureSingleAssetVault)) - return temDISABLED; - - if (auto const ter = preflight1(ctx)) - return ter; - - if (ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; - if (ctx.tx[sfVaultID] == beast::zero) { JLOG(ctx.j.debug()) << "VaultWithdraw: zero/empty vault ID."; @@ -68,7 +59,7 @@ VaultWithdraw::preflight(PreflightContext const& ctx) return temMALFORMED; } - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/XChainBridge.cpp b/src/xrpld/app/tx/detail/XChainBridge.cpp index 6ca049ee66..2587845df5 100644 --- a/src/xrpld/app/tx/detail/XChainBridge.cpp +++ b/src/xrpld/app/tx/detail/XChainBridge.cpp @@ -21,15 +21,15 @@ #include #include #include -#include -#include -#include #include #include #include #include #include +#include +#include +#include #include #include #include @@ -1210,17 +1210,8 @@ toClaim(STTx const& tx) template NotTEC -attestationPreflight(PreflightContext const& ctx) +attestationpreflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureXChainBridge)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; - if (!publicKeyType(ctx.tx[sfPublicKey])) return temMALFORMED; @@ -1241,7 +1232,7 @@ attestationPreflight(PreflightContext const& ctx) if (att->sendingAmount.issue() != expectedIssue) return temXCHAIN_BAD_PROOF; - return preflight2(ctx); + return tesSUCCESS; } template @@ -1379,15 +1370,6 @@ attestationDoApply(ApplyContext& ctx) NotTEC XChainCreateBridge::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureXChainBridge)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; - auto const account = ctx.tx[sfAccount]; auto const reward = ctx.tx[sfSignatureReward]; auto const minAccountCreate = ctx.tx[~sfMinAccountCreateAmount]; @@ -1457,7 +1439,7 @@ XChainCreateBridge::preflight(PreflightContext const& ctx) return temXCHAIN_BRIDGE_BAD_ISSUES; } - return preflight2(ctx); + return tesSUCCESS; } TER @@ -1557,18 +1539,15 @@ XChainCreateBridge::doApply() //------------------------------------------------------------------------------ +std::uint32_t +BridgeModify::getFlagsMask(PreflightContext const& ctx) +{ + return tfBridgeModifyMask; +} + NotTEC BridgeModify::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureXChainBridge)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfBridgeModifyMask) - return temINVALID_FLAG; - auto const account = ctx.tx[sfAccount]; auto const reward = ctx.tx[~sfSignatureReward]; auto const minAccountCreate = ctx.tx[~sfMinAccountCreateAmount]; @@ -1607,7 +1586,7 @@ BridgeModify::preflight(PreflightContext const& ctx) return temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT; } - return preflight2(ctx); + return tesSUCCESS; } TER @@ -1670,15 +1649,6 @@ BridgeModify::doApply() NotTEC XChainClaim::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureXChainBridge)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; - STXChainBridge const bridgeSpec = ctx.tx[sfXChainBridge]; auto const amount = ctx.tx[sfAmount]; @@ -1689,7 +1659,7 @@ XChainClaim::preflight(PreflightContext const& ctx) return temBAD_AMOUNT; } - return preflight2(ctx); + return tesSUCCESS; } TER @@ -1908,15 +1878,6 @@ XChainCommit::makeTxConsequences(PreflightContext const& ctx) NotTEC XChainCommit::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureXChainBridge)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; - auto const amount = ctx.tx[sfAmount]; auto const bridgeSpec = ctx.tx[sfXChainBridge]; @@ -1927,7 +1888,7 @@ XChainCommit::preflight(PreflightContext const& ctx) amount.issue() != bridgeSpec.issuingChainIssue()) return temBAD_ISSUER; - return preflight2(ctx); + return tesSUCCESS; } TER @@ -2022,21 +1983,12 @@ XChainCommit::doApply() NotTEC XChainCreateClaimID::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureXChainBridge)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; - auto const reward = ctx.tx[sfSignatureReward]; if (!isXRP(reward) || reward.signum() < 0 || !isLegalNet(reward)) return temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT; - return preflight2(ctx); + return tesSUCCESS; } TER @@ -2137,7 +2089,7 @@ XChainCreateClaimID::doApply() NotTEC XChainAddClaimAttestation::preflight(PreflightContext const& ctx) { - return attestationPreflight(ctx); + return attestationpreflight(ctx); } TER @@ -2157,7 +2109,7 @@ XChainAddClaimAttestation::doApply() NotTEC XChainAddAccountCreateAttestation::preflight(PreflightContext const& ctx) { - return attestationPreflight(ctx); + return attestationpreflight(ctx); } TER @@ -2177,15 +2129,6 @@ XChainAddAccountCreateAttestation::doApply() NotTEC XChainCreateAccountCommit::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureXChainBridge)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; - auto const amount = ctx.tx[sfAmount]; if (amount.signum() <= 0 || !amount.native()) @@ -2198,7 +2141,7 @@ XChainCreateAccountCommit::preflight(PreflightContext const& ctx) if (reward.issue() != amount.issue()) return temBAD_AMOUNT; - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/XChainBridge.h b/src/xrpld/app/tx/detail/XChainBridge.h index 82b64cc0e3..0e9c0358d2 100644 --- a/src/xrpld/app/tx/detail/XChainBridge.h +++ b/src/xrpld/app/tx/detail/XChainBridge.h @@ -58,6 +58,9 @@ public: { } + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/applySteps.cpp b/src/xrpld/app/tx/detail/applySteps.cpp index 03ef7244f8..c2e4e13f08 100644 --- a/src/xrpld/app/tx/detail/applySteps.cpp +++ b/src/xrpld/app/tx/detail/applySteps.cpp @@ -18,57 +18,20 @@ //============================================================================== #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#pragma push_macro("TRANSACTION") +#undef TRANSACTION + +// Do nothing +#define TRANSACTION(...) +#define TRANSACTION_INCLUDE 1 + +#include + +#undef TRANSACTION +#pragma pop_macro("TRANSACTION") + +// DO NOT INCLUDE TRANSACTOR HEADER FILES HERE. +// See the instructions at the top of transactions.macro instead. #include @@ -156,7 +119,7 @@ invoke_preflight(PreflightContext const& ctx) try { return with_txn_type(ctx.tx.getTxnType(), [&]() { - auto const tec = T::preflight(ctx); + auto const tec = Transactor::invokePreflight(ctx); return std::make_pair( tec, isTesSuccess(tec) ? consequences_helper(ctx) diff --git a/src/xrpld/core/detail/semaphore.h b/src/xrpld/core/detail/semaphore.h index 3b64265bb1..fbeb79c66a 100644 --- a/src/xrpld/core/detail/semaphore.h +++ b/src/xrpld/core/detail/semaphore.h @@ -17,6 +17,34 @@ */ //============================================================================== +/** + * + * TODO: Remove ripple::basic_semaphore (and this file) and use + * std::counting_semaphore. + * + * Background: + * - PR: https://github.com/XRPLF/rippled/pull/5512/files + * - std::counting_semaphore had a bug fixed in both GCC and Clang: + * * GCC PR 104928: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=104928 + * * LLVM PR 79265: https://github.com/llvm/llvm-project/pull/79265 + * + * GCC: + * According to GCC Bugzilla PR104928 + * (https://gcc.gnu.org/bugzilla/show_bug.cgi?id=104928#c15), the fix is + * scheduled for inclusion in GCC 16.0 (see comment #15, Target + * Milestone: 16.0). It is not included in GCC 14.x or earlier, and there is no + * indication that it will be backported to GCC 13.x or 14.x branches. + * + * Clang: + * The fix for is included in Clang 19.1.0+ + * + * Once the minimum compiler version is updated to > GCC 16.0 or Clang 19.1.0, + * we can remove this file. + * + * WARNING: Avoid using std::counting_semaphore until the minimum compiler + * version is updated. + */ + #ifndef RIPPLE_CORE_SEMAPHORE_H_INCLUDED #define RIPPLE_CORE_SEMAPHORE_H_INCLUDED diff --git a/src/xrpld/overlay/detail/ConnectAttempt.cpp b/src/xrpld/overlay/detail/ConnectAttempt.cpp index 397ac06ba6..c1bc4bb069 100644 --- a/src/xrpld/overlay/detail/ConnectAttempt.cpp +++ b/src/xrpld/overlay/detail/ConnectAttempt.cpp @@ -24,6 +24,8 @@ #include +#include + namespace ripple { ConnectAttempt::ConnectAttempt( @@ -45,6 +47,7 @@ ConnectAttempt::ConnectAttempt( , usage_(usage) , strand_(boost::asio::make_strand(io_context)) , timer_(io_context) + , stepTimer_(io_context) , stream_ptr_(std::make_unique( socket_type(std::forward(io_context)), *context)) @@ -52,14 +55,14 @@ ConnectAttempt::ConnectAttempt( , stream_(*stream_ptr_) , slot_(slot) { - JLOG(journal_.debug()) << "Connect " << remote_endpoint; } ConnectAttempt::~ConnectAttempt() { + // slot_ will be null if we successfully connected + // and transferred ownership to a PeerImp if (slot_ != nullptr) overlay_.peerFinder().on_closed(slot_); - JLOG(journal_.trace()) << "~ConnectAttempt"; } void @@ -68,16 +71,29 @@ ConnectAttempt::stop() if (!strand_.running_in_this_thread()) return boost::asio::post( strand_, std::bind(&ConnectAttempt::stop, shared_from_this())); - if (socket_.is_open()) - { - JLOG(journal_.debug()) << "Stop"; - } - close(); + + if (!socket_.is_open()) + return; + + JLOG(journal_.debug()) << "stop: Stop"; + + shutdown(); } void ConnectAttempt::run() { + if (!strand_.running_in_this_thread()) + return boost::asio::post( + strand_, std::bind(&ConnectAttempt::run, shared_from_this())); + + JLOG(journal_.debug()) << "run: connecting to " << remote_endpoint_; + + ioPending_ = true; + + // Allow up to connectTimeout_ seconds to establish remote peer connection + setTimer(ConnectionStep::TcpConnect); + stream_.next_layer().async_connect( remote_endpoint_, boost::asio::bind_executor( @@ -90,61 +106,177 @@ ConnectAttempt::run() //------------------------------------------------------------------------------ +void +ConnectAttempt::shutdown() +{ + XRPL_ASSERT( + strand_.running_in_this_thread(), + "ripple::ConnectAttempt::shutdown: strand in this thread"); + + if (!socket_.is_open()) + return; + + shutdown_ = true; + boost::beast::get_lowest_layer(stream_).cancel(); + + tryAsyncShutdown(); +} + +void +ConnectAttempt::tryAsyncShutdown() +{ + XRPL_ASSERT( + strand_.running_in_this_thread(), + "ripple::ConnectAttempt::tryAsyncShutdown : strand in this thread"); + + if (!shutdown_ || currentStep_ == ConnectionStep::ShutdownStarted) + return; + + if (ioPending_) + return; + + // gracefully shutdown the SSL socket, performing a shutdown handshake + if (currentStep_ != ConnectionStep::TcpConnect && + currentStep_ != ConnectionStep::TlsHandshake) + { + setTimer(ConnectionStep::ShutdownStarted); + return stream_.async_shutdown(bind_executor( + strand_, + std::bind( + &ConnectAttempt::onShutdown, + shared_from_this(), + std::placeholders::_1))); + } + + close(); +} + +void +ConnectAttempt::onShutdown(error_code ec) +{ + cancelTimer(); + + if (ec) + { + // - eof: the stream was cleanly closed + // - operation_aborted: an expired timer (slow shutdown) + // - stream_truncated: the tcp connection closed (no handshake) it could + // occur if a peer does not perform a graceful disconnect + // - broken_pipe: the peer is gone + // - application data after close notify: benign SSL shutdown condition + bool shouldLog = + (ec != boost::asio::error::eof && + ec != boost::asio::error::operation_aborted && + ec.message().find("application data after close notify") == + std::string::npos); + + if (shouldLog) + { + JLOG(journal_.debug()) << "onShutdown: " << ec.message(); + } + } + + close(); +} + void ConnectAttempt::close() { XRPL_ASSERT( strand_.running_in_this_thread(), "ripple::ConnectAttempt::close : strand in this thread"); - if (socket_.is_open()) - { - try - { - timer_.cancel(); - socket_.close(); - } - catch (boost::system::system_error const&) - { - // ignored - } + if (!socket_.is_open()) + return; - JLOG(journal_.debug()) << "Closed"; - } + cancelTimer(); + + error_code ec; + socket_.close(ec); } void ConnectAttempt::fail(std::string const& reason) { JLOG(journal_.debug()) << reason; - close(); + shutdown(); } void ConnectAttempt::fail(std::string const& name, error_code ec) { JLOG(journal_.debug()) << name << ": " << ec.message(); - close(); + shutdown(); } void -ConnectAttempt::setTimer() +ConnectAttempt::setTimer(ConnectionStep step) { - try + currentStep_ = step; + + // Set global timer (only if not already set) + if (timer_.expiry() == std::chrono::steady_clock::time_point{}) { - timer_.expires_after(std::chrono::seconds(15)); - } - catch (boost::system::system_error const& e) - { - JLOG(journal_.error()) << "setTimer: " << e.code(); - return; + try + { + timer_.expires_after(connectTimeout); + timer_.async_wait(boost::asio::bind_executor( + strand_, + std::bind( + &ConnectAttempt::onTimer, + shared_from_this(), + std::placeholders::_1))); + } + catch (std::exception const& ex) + { + JLOG(journal_.error()) << "setTimer (global): " << ex.what(); + return close(); + } } - timer_.async_wait(boost::asio::bind_executor( - strand_, - std::bind( - &ConnectAttempt::onTimer, - shared_from_this(), - std::placeholders::_1))); + // Set step-specific timer + try + { + std::chrono::seconds stepTimeout; + switch (step) + { + case ConnectionStep::TcpConnect: + stepTimeout = StepTimeouts::tcpConnect; + break; + case ConnectionStep::TlsHandshake: + stepTimeout = StepTimeouts::tlsHandshake; + break; + case ConnectionStep::HttpWrite: + stepTimeout = StepTimeouts::httpWrite; + break; + case ConnectionStep::HttpRead: + stepTimeout = StepTimeouts::httpRead; + break; + case ConnectionStep::ShutdownStarted: + stepTimeout = StepTimeouts::tlsShutdown; + break; + case ConnectionStep::Complete: + case ConnectionStep::Init: + return; // No timer needed for init or complete step + } + + // call to expires_after cancels previous timer + stepTimer_.expires_after(stepTimeout); + stepTimer_.async_wait(boost::asio::bind_executor( + strand_, + std::bind( + &ConnectAttempt::onTimer, + shared_from_this(), + std::placeholders::_1))); + + JLOG(journal_.trace()) << "setTimer: " << stepToString(step) + << " timeout=" << stepTimeout.count() << "s"; + } + catch (std::exception const& ex) + { + JLOG(journal_.error()) + << "setTimer (step " << stepToString(step) << "): " << ex.what(); + return close(); + } } void @@ -153,6 +285,7 @@ ConnectAttempt::cancelTimer() try { timer_.cancel(); + stepTimer_.cancel(); } catch (boost::system::system_error const&) { @@ -165,34 +298,69 @@ ConnectAttempt::onTimer(error_code ec) { if (!socket_.is_open()) return; - if (ec == boost::asio::error::operation_aborted) - return; + if (ec) { + // do not initiate shutdown, timers are frequently cancelled + if (ec == boost::asio::error::operation_aborted) + return; + // This should never happen JLOG(journal_.error()) << "onTimer: " << ec.message(); return close(); } - fail("Timeout"); + + // Determine which timer expired by checking their expiry times + auto const now = std::chrono::steady_clock::now(); + bool globalExpired = (timer_.expiry() <= now); + bool stepExpired = (stepTimer_.expiry() <= now); + + if (globalExpired) + { + JLOG(journal_.debug()) + << "onTimer: Global timeout; step: " << stepToString(currentStep_); + } + else if (stepExpired) + { + JLOG(journal_.debug()) + << "onTimer: Step timeout; step: " << stepToString(currentStep_); + } + else + { + JLOG(journal_.warn()) << "onTimer: Unexpected timer callback"; + } + + close(); } void ConnectAttempt::onConnect(error_code ec) { - cancelTimer(); + ioPending_ = false; - if (ec == boost::asio::error::operation_aborted) - return; - endpoint_type local_endpoint; - if (!ec) - local_endpoint = socket_.local_endpoint(ec); if (ec) + { + if (ec == boost::asio::error::operation_aborted) + return tryAsyncShutdown(); + return fail("onConnect", ec); + } + if (!socket_.is_open()) return; - JLOG(journal_.trace()) << "onConnect"; - setTimer(); + // check if connection has really been established + socket_.local_endpoint(ec); + if (ec) + return fail("onConnect", ec); + + if (shutdown_) + return tryAsyncShutdown(); + + ioPending_ = true; + + setTimer(ConnectionStep::TlsHandshake); + stream_.set_verify_mode(boost::asio::ssl::verify_none); stream_.async_handshake( boost::asio::ssl::stream_base::client, @@ -207,25 +375,30 @@ ConnectAttempt::onConnect(error_code ec) void ConnectAttempt::onHandshake(error_code ec) { - cancelTimer(); - if (!socket_.is_open()) - return; - if (ec == boost::asio::error::operation_aborted) - return; - endpoint_type local_endpoint; - if (!ec) - local_endpoint = socket_.local_endpoint(ec); + ioPending_ = false; + + if (ec) + { + if (ec == boost::asio::error::operation_aborted) + return tryAsyncShutdown(); + + return fail("onHandshake", ec); + } + + auto const local_endpoint = socket_.local_endpoint(ec); if (ec) return fail("onHandshake", ec); - JLOG(journal_.trace()) << "onHandshake"; + setTimer(ConnectionStep::HttpWrite); + + // check if we connected to ourselves if (!overlay_.peerFinder().onConnected( slot_, beast::IPAddressConversion::from_asio(local_endpoint))) - return fail("Duplicate connection"); + return fail("Self connection"); auto const sharedValue = makeSharedValue(*stream_ptr_, journal_); if (!sharedValue) - return close(); // makeSharedValue logs + return shutdown(); // makeSharedValue logs req_ = makeRequest( !overlay_.peerFinder().config().peerPrivate, @@ -242,7 +415,11 @@ ConnectAttempt::onHandshake(error_code ec) remote_endpoint_.address(), app_); - setTimer(); + if (shutdown_) + return tryAsyncShutdown(); + + ioPending_ = true; + boost::beast::http::async_write( stream_, req_, @@ -257,13 +434,23 @@ ConnectAttempt::onHandshake(error_code ec) void ConnectAttempt::onWrite(error_code ec) { - cancelTimer(); - if (!socket_.is_open()) - return; - if (ec == boost::asio::error::operation_aborted) - return; + ioPending_ = false; + if (ec) + { + if (ec == boost::asio::error::operation_aborted) + return tryAsyncShutdown(); + return fail("onWrite", ec); + } + + if (shutdown_) + return tryAsyncShutdown(); + + ioPending_ = true; + + setTimer(ConnectionStep::HttpRead); + boost::beast::http::async_read( stream_, read_buf_, @@ -280,39 +467,27 @@ void ConnectAttempt::onRead(error_code ec) { cancelTimer(); + ioPending_ = false; + currentStep_ = ConnectionStep::Complete; - if (!socket_.is_open()) - return; - if (ec == boost::asio::error::operation_aborted) - return; - if (ec == boost::asio::error::eof) - { - JLOG(journal_.info()) << "EOF"; - setTimer(); - return stream_.async_shutdown(boost::asio::bind_executor( - strand_, - std::bind( - &ConnectAttempt::onShutdown, - shared_from_this(), - std::placeholders::_1))); - } if (ec) - return fail("onRead", ec); - processResponse(); -} - -void -ConnectAttempt::onShutdown(error_code ec) -{ - cancelTimer(); - if (!ec) { - JLOG(journal_.error()) << "onShutdown: expected error condition"; - return close(); + if (ec == boost::asio::error::eof) + { + JLOG(journal_.debug()) << "EOF"; + return shutdown(); + } + + if (ec == boost::asio::error::operation_aborted) + return tryAsyncShutdown(); + + return fail("onRead", ec); } - if (ec != boost::asio::error::eof) - return fail("onShutdown", ec); - close(); + + if (shutdown_) + return tryAsyncShutdown(); + + processResponse(); } //-------------------------------------------------------------------------- @@ -320,48 +495,69 @@ ConnectAttempt::onShutdown(error_code ec) void ConnectAttempt::processResponse() { - if (response_.result() == boost::beast::http::status::service_unavailable) - { - Json::Value json; - Json::Reader r; - std::string s; - s.reserve(boost::asio::buffer_size(response_.body().data())); - for (auto const buffer : response_.body().data()) - s.append( - static_cast(buffer.data()), - boost::asio::buffer_size(buffer)); - auto const success = r.parse(s, json); - if (success) - { - if (json.isObject() && json.isMember("peer-ips")) - { - Json::Value const& ips = json["peer-ips"]; - if (ips.isArray()) - { - std::vector eps; - eps.reserve(ips.size()); - for (auto const& v : ips) - { - if (v.isString()) - { - error_code ec; - auto const ep = parse_endpoint(v.asString(), ec); - if (!ec) - eps.push_back(ep); - } - } - overlay_.peerFinder().onRedirects(remote_endpoint_, eps); - } - } - } - } - if (!OverlayImpl::isPeerUpgrade(response_)) { - JLOG(journal_.info()) - << "Unable to upgrade to peer protocol: " << response_.result() - << " (" << response_.reason() << ")"; - return close(); + // A peer may respond with service_unavailable and a list of alternative + // peers to connect to, a differing status code is unexpected + if (response_.result() != + boost::beast::http::status::service_unavailable) + { + JLOG(journal_.warn()) + << "Unable to upgrade to peer protocol: " << response_.result() + << " (" << response_.reason() << ")"; + return shutdown(); + } + + // Parse response body to determine if this is a redirect or other + // service unavailable + std::string responseBody; + responseBody.reserve(boost::asio::buffer_size(response_.body().data())); + for (auto const buffer : response_.body().data()) + responseBody.append( + static_cast(buffer.data()), + boost::asio::buffer_size(buffer)); + + Json::Value json; + Json::Reader reader; + auto const isValidJson = reader.parse(responseBody, json); + + // Check if this is a redirect response (contains peer-ips field) + auto const isRedirect = + isValidJson && json.isObject() && json.isMember("peer-ips"); + + if (!isRedirect) + { + JLOG(journal_.warn()) + << "processResponse: " << remote_endpoint_ + << " failed to upgrade to peer protocol: " << response_.result() + << " (" << response_.reason() << ")"; + + return shutdown(); + } + + Json::Value const& peerIps = json["peer-ips"]; + if (!peerIps.isArray()) + return fail("processResponse: invalid peer-ips format"); + + // Extract and validate peer endpoints + std::vector redirectEndpoints; + redirectEndpoints.reserve(peerIps.size()); + + for (auto const& ipValue : peerIps) + { + if (!ipValue.isString()) + continue; + + error_code ec; + auto const endpoint = parse_endpoint(ipValue.asString(), ec); + if (!ec) + redirectEndpoints.push_back(endpoint); + } + + // Notify PeerFinder about the redirect redirectEndpoints may be empty + overlay_.peerFinder().onRedirects(remote_endpoint_, redirectEndpoints); + + return fail("processResponse: failed to connect to peer: redirected"); } // Just because our peer selected a particular protocol version doesn't @@ -381,11 +577,11 @@ ConnectAttempt::processResponse() auto const sharedValue = makeSharedValue(*stream_ptr_, journal_); if (!sharedValue) - return close(); // makeSharedValue logs + return shutdown(); // makeSharedValue logs try { - auto publicKey = verifyHandshake( + auto const publicKey = verifyHandshake( response_, *sharedValue, overlay_.setup().networkID, @@ -393,11 +589,10 @@ ConnectAttempt::processResponse() remote_endpoint_.address(), app_); - JLOG(journal_.info()) - << "Public Key: " << toBase58(TokenType::NodePublic, publicKey); - JLOG(journal_.debug()) << "Protocol: " << to_string(*negotiatedProtocol); + JLOG(journal_.info()) + << "Public Key: " << toBase58(TokenType::NodePublic, publicKey); auto const member = app_.cluster().member(publicKey); if (member) @@ -405,10 +600,21 @@ ConnectAttempt::processResponse() JLOG(journal_.info()) << "Cluster name: " << *member; } - auto const result = overlay_.peerFinder().activate( - slot_, publicKey, static_cast(member)); + auto const result = + overlay_.peerFinder().activate(slot_, publicKey, !member->empty()); if (result != PeerFinder::Result::success) - return fail("Outbound " + std::string(to_string(result))); + { + std::stringstream ss; + ss << "Outbound Connect Attempt " << remote_endpoint_ << " " + << to_string(result); + return fail(ss.str()); + } + + if (!socket_.is_open()) + return; + + if (shutdown_) + return tryAsyncShutdown(); auto const peer = std::make_shared( app_, diff --git a/src/xrpld/overlay/detail/ConnectAttempt.h b/src/xrpld/overlay/detail/ConnectAttempt.h index febbe88f45..38b9482d9d 100644 --- a/src/xrpld/overlay/detail/ConnectAttempt.h +++ b/src/xrpld/overlay/detail/ConnectAttempt.h @@ -22,90 +22,258 @@ #include +#include + namespace ripple { -/** Manages an outbound connection attempt. */ +/** + * @class ConnectAttempt + * @brief Manages outbound peer connection attempts with comprehensive timeout + * handling + * + * The ConnectAttempt class handles the complete lifecycle of establishing an + * outbound connection to a peer in the XRPL network. It implements a + * sophisticated dual-timer system that provides both global timeout protection + * and per-step timeout diagnostics. + * + * The connection establishment follows these steps: + * 1. **TCP Connect**: Establish basic network connection + * 2. **TLS Handshake**: Negotiate SSL/TLS encryption + * 3. **HTTP Write**: Send peer handshake request + * 4. **HTTP Read**: Receive and validate peer response + * 5. **Complete**: Connection successfully established + * + * Uses a hybrid timeout approach: + * - **Global Timer**: Hard limit (20s) for entire connection process + * - **Step Timers**: Individual timeouts for each connection phase + * + * - All errors result in connection termination + * + * All operations are serialized using boost::asio::strand to ensure thread + * safety. The class is designed to be used exclusively within the ASIO event + * loop. + * + * @note This class should not be used directly. It is managed by OverlayImpl + * as part of the peer discovery and connection management system. + * + */ class ConnectAttempt : public OverlayImpl::Child, public std::enable_shared_from_this { private: using error_code = boost::system::error_code; - using endpoint_type = boost::asio::ip::tcp::endpoint; - using request_type = boost::beast::http::request; - using response_type = boost::beast::http::response; - using socket_type = boost::asio::ip::tcp::socket; using middle_type = boost::beast::tcp_stream; using stream_type = boost::beast::ssl_stream; using shared_context = std::shared_ptr; + /** + * @enum ConnectionStep + * @brief Represents the current phase of the connection establishment + * process + * + * Used for tracking progress and providing detailed timeout diagnostics. + * Each step has its own timeout value defined in StepTimeouts. + */ + enum class ConnectionStep { + Init, // Initial state, nothing started + TcpConnect, // Establishing TCP connection to remote peer + TlsHandshake, // Performing SSL/TLS handshake + HttpWrite, // Sending HTTP upgrade request + HttpRead, // Reading HTTP upgrade response + Complete, // Connection successfully established + ShutdownStarted // Connection shutdown has started + }; + + // A timeout for connection process, greater than all step timeouts + static constexpr std::chrono::seconds connectTimeout{25}; + + /** + * @struct StepTimeouts + * @brief Defines timeout values for each connection step + * + * These timeouts are designed to detect slow individual phases while + * allowing the global timeout to enforce the overall time limit. + */ + struct StepTimeouts + { + // TCP connection timeout + static constexpr std::chrono::seconds tcpConnect{8}; + // SSL handshake timeout + static constexpr std::chrono::seconds tlsHandshake{8}; + // HTTP write timeout + static constexpr std::chrono::seconds httpWrite{3}; + // HTTP read timeout + static constexpr std::chrono::seconds httpRead{3}; + // SSL shutdown timeout + static constexpr std::chrono::seconds tlsShutdown{2}; + }; + + // Core application and networking components Application& app_; - std::uint32_t const id_; + Peer::id_t const id_; beast::WrappedSink sink_; beast::Journal const journal_; endpoint_type remote_endpoint_; Resource::Consumer usage_; + boost::asio::strand strand_; boost::asio::basic_waitable_timer timer_; - std::unique_ptr stream_ptr_; + boost::asio::basic_waitable_timer stepTimer_; + + std::unique_ptr stream_ptr_; // SSL stream (owned) socket_type& socket_; stream_type& stream_; boost::beast::multi_buffer read_buf_; + response_type response_; std::shared_ptr slot_; request_type req_; + bool shutdown_ = false; // Shutdown has been initiated + bool ioPending_ = false; // Async I/O operation in progress + ConnectionStep currentStep_ = ConnectionStep::Init; + public: + /** + * @brief Construct a new ConnectAttempt object + * + * @param app Application context providing configuration and services + * @param io_context ASIO I/O context for async operations + * @param remote_endpoint Target peer endpoint to connect to + * @param usage Resource usage tracker for rate limiting + * @param context Shared SSL context for encryption + * @param id Unique peer identifier for this connection attempt + * @param slot PeerFinder slot representing this connection + * @param journal Logging interface for diagnostics + * @param overlay Parent overlay manager + * + * @note The constructor only initializes the object. Call run() to begin + * the actual connection attempt. + */ ConnectAttempt( Application& app, boost::asio::io_context& io_context, endpoint_type const& remote_endpoint, Resource::Consumer usage, shared_context const& context, - std::uint32_t id, + Peer::id_t id, std::shared_ptr const& slot, beast::Journal journal, OverlayImpl& overlay); ~ConnectAttempt(); + /** + * @brief Stop the connection attempt + * + * This method is thread-safe and can be called from any thread. + */ void stop() override; + /** + * @brief Begin the connection attempt + * + * This method is thread-safe and posts to the strand if needed. + */ void run(); private: + /** + * @brief Set timers for the specified connection step + * + * @param step The connection step to set timers for + * + * Sets both the step-specific timer and the global timer (if not already + * set). + */ void - close(); - void - fail(std::string const& reason); - void - fail(std::string const& name, error_code ec); - void - setTimer(); + setTimer(ConnectionStep step); + + /** + * @brief Cancel both global and step timers + * + * Used during cleanup and when connection completes successfully. + * Exceptions from timer cancellation are safely ignored. + */ void cancelTimer(); + + /** + * @brief Handle timer expiration events + * + * @param ec Error code from timer operation + * + * Determines which timer expired (global vs step) and logs appropriate + * diagnostic information before terminating the connection. + */ void onTimer(error_code ec); + + // Connection phase handlers void - onConnect(error_code ec); + onConnect(error_code ec); // TCP connection completion handler void - onHandshake(error_code ec); + onHandshake(error_code ec); // TLS handshake completion handler void - onWrite(error_code ec); + onWrite(error_code ec); // HTTP write completion handler void - onRead(error_code ec); + onRead(error_code ec); // HTTP read completion handler + + // Error and cleanup handlers void - onShutdown(error_code ec); + fail(std::string const& reason); // Fail with custom reason + void + fail(std::string const& name, error_code ec); // Fail with system error + void + shutdown(); // Initiate graceful shutdown + void + tryAsyncShutdown(); // Attempt async SSL shutdown + void + onShutdown(error_code ec); // SSL shutdown completion handler + void + close(); // Force close socket + + /** + * @brief Process the HTTP upgrade response from peer + * + * Validates the peer's response, extracts protocol information, + * verifies handshake, and either creates a PeerImp or handles + * redirect responses. + */ void processResponse(); + static std::string + stepToString(ConnectionStep step) + { + switch (step) + { + case ConnectionStep::Init: + return "Init"; + case ConnectionStep::TcpConnect: + return "TcpConnect"; + case ConnectionStep::TlsHandshake: + return "TlsHandshake"; + case ConnectionStep::HttpWrite: + return "HttpWrite"; + case ConnectionStep::HttpRead: + return "HttpRead"; + case ConnectionStep::Complete: + return "Complete"; + case ConnectionStep::ShutdownStarted: + return "ShutdownStarted"; + } + return "Unknown"; + }; + template static boost::asio::ip::tcp::endpoint parse_endpoint(std::string const& s, boost::system::error_code& ec) diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index 69f25e1eb4..93371f42ab 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -44,6 +44,7 @@ #include #include +#include #include #include #include @@ -59,6 +60,10 @@ std::chrono::milliseconds constexpr peerHighLatency{300}; /** How often we PING the peer to check for latency and sendq probe */ std::chrono::seconds constexpr peerTimerInterval{60}; + +/** The timeout for a shutdown timer */ +std::chrono::seconds constexpr shutdownTimerInterval{5}; + } // namespace // TODO: Remove this exclusion once unit tests are added after the hotfix @@ -215,23 +220,17 @@ PeerImp::stop() { if (!strand_.running_in_this_thread()) return post(strand_, std::bind(&PeerImp::stop, shared_from_this())); - if (socket_.is_open()) - { - // The rationale for using different severity levels is that - // outbound connections are under our control and may be logged - // at a higher level, but inbound connections are more numerous and - // uncontrolled so to prevent log flooding the severity is reduced. - // - if (inbound_) - { - JLOG(journal_.debug()) << "Stop"; - } - else - { - JLOG(journal_.info()) << "Stop"; - } - } - close(); + + if (!socket_.is_open()) + return; + + // The rationale for using different severity levels is that + // outbound connections are under our control and may be logged + // at a higher level, but inbound connections are more numerous and + // uncontrolled so to prevent log flooding the severity is reduced. + JLOG(journal_.debug()) << "stop: Stop"; + + shutdown(); } //------------------------------------------------------------------------------ @@ -241,11 +240,14 @@ PeerImp::send(std::shared_ptr const& m) { if (!strand_.running_in_this_thread()) return post(strand_, std::bind(&PeerImp::send, shared_from_this(), m)); - if (gracefulClose_) - return; - if (detaching_) + + if (!socket_.is_open()) return; + // we are in progress of closing the connection + if (shutdown_) + return tryAsyncShutdown(); + auto validator = m->getValidatorKey(); if (validator && !squelch_.expireSquelch(*validator)) { @@ -287,6 +289,7 @@ PeerImp::send(std::shared_ptr const& m) if (sendq_size != 0) return; + writePending_ = true; boost::asio::async_write( stream_, boost::asio::buffer( @@ -573,34 +576,21 @@ PeerImp::hasRange(std::uint32_t uMin, std::uint32_t uMax) //------------------------------------------------------------------------------ void -PeerImp::close() +PeerImp::fail(std::string const& name, error_code ec) { XRPL_ASSERT( strand_.running_in_this_thread(), - "ripple::PeerImp::close : strand in this thread"); - if (socket_.is_open()) - { - detaching_ = true; // DEPRECATED - try - { - timer_.cancel(); - socket_.close(); - } - catch (boost::system::system_error const&) - { - // ignored - } + "ripple::PeerImp::fail : strand in this thread"); - overlay_.incPeerDisconnect(); - if (inbound_) - { - JLOG(journal_.debug()) << "Closed"; - } - else - { - JLOG(journal_.info()) << "Closed"; - } - } + if (!socket_.is_open()) + return; + + JLOG(journal_.warn()) << name << " from " + << toBase58(TokenType::NodePublic, publicKey_) + << " at " << remote_address_.to_string() << ": " + << ec.message(); + + shutdown(); } void @@ -613,45 +603,39 @@ PeerImp::fail(std::string const& reason) (void(Peer::*)(std::string const&)) & PeerImp::fail, shared_from_this(), reason)); - if (journal_.active(beast::severities::kWarning) && socket_.is_open()) + + if (!socket_.is_open()) + return; + + // Call to name() locks, log only if the message will be outputed + if (journal_.active(beast::severities::kWarning)) { std::string const n = name(); JLOG(journal_.warn()) << (n.empty() ? remote_address_.to_string() : n) << " failed: " << reason; } - close(); + + shutdown(); } void -PeerImp::fail(std::string const& name, error_code ec) +PeerImp::tryAsyncShutdown() { XRPL_ASSERT( strand_.running_in_this_thread(), - "ripple::PeerImp::fail : strand in this thread"); - if (socket_.is_open()) - { - JLOG(journal_.warn()) - << name << " from " << toBase58(TokenType::NodePublic, publicKey_) - << " at " << remote_address_.to_string() << ": " << ec.message(); - } - close(); -} + "ripple::PeerImp::tryAsyncShutdown : strand in this thread"); -void -PeerImp::gracefulClose() -{ - XRPL_ASSERT( - strand_.running_in_this_thread(), - "ripple::PeerImp::gracefulClose : strand in this thread"); - XRPL_ASSERT( - socket_.is_open(), "ripple::PeerImp::gracefulClose : socket is open"); - XRPL_ASSERT( - !gracefulClose_, - "ripple::PeerImp::gracefulClose : socket is not closing"); - gracefulClose_ = true; - if (send_queue_.size() > 0) + if (!shutdown_ || shutdownStarted_) return; - setTimer(); + + if (readPending_ || writePending_) + return; + + shutdownStarted_ = true; + + setTimer(shutdownTimerInterval); + + // gracefully shutdown the SSL socket, performing a shutdown handshake stream_.async_shutdown(bind_executor( strand_, std::bind( @@ -659,69 +643,125 @@ PeerImp::gracefulClose() } void -PeerImp::setTimer() +PeerImp::shutdown() +{ + XRPL_ASSERT( + strand_.running_in_this_thread(), + "ripple::PeerImp::shutdown: strand in this thread"); + + if (!socket_.is_open() || shutdown_) + return; + + shutdown_ = true; + + boost::beast::get_lowest_layer(stream_).cancel(); + + tryAsyncShutdown(); +} + +void +PeerImp::onShutdown(error_code ec) +{ + cancelTimer(); + if (ec) + { + // - eof: the stream was cleanly closed + // - operation_aborted: an expired timer (slow shutdown) + // - stream_truncated: the tcp connection closed (no handshake) it could + // occur if a peer does not perform a graceful disconnect + // - broken_pipe: the peer is gone + bool shouldLog = + (ec != boost::asio::error::eof && + ec != boost::asio::error::operation_aborted && + ec.message().find("application data after close notify") == + std::string::npos); + + if (shouldLog) + { + JLOG(journal_.debug()) << "onShutdown: " << ec.message(); + } + } + + close(); +} + +void +PeerImp::close() +{ + XRPL_ASSERT( + strand_.running_in_this_thread(), + "ripple::PeerImp::close : strand in this thread"); + + if (!socket_.is_open()) + return; + + cancelTimer(); + + error_code ec; + socket_.close(ec); + + overlay_.incPeerDisconnect(); + + // The rationale for using different severity levels is that + // outbound connections are under our control and may be logged + // at a higher level, but inbound connections are more numerous and + // uncontrolled so to prevent log flooding the severity is reduced. + JLOG((inbound_ ? journal_.debug() : journal_.info())) << "close: Closed"; +} + +//------------------------------------------------------------------------------ + +void +PeerImp::setTimer(std::chrono::seconds interval) { try { - timer_.expires_after(peerTimerInterval); + timer_.expires_after(interval); } - catch (boost::system::system_error const& e) + catch (std::exception const& ex) { - JLOG(journal_.error()) << "setTimer: " << e.code(); - return; + JLOG(journal_.error()) << "setTimer: " << ex.what(); + return shutdown(); } + timer_.async_wait(bind_executor( strand_, std::bind( &PeerImp::onTimer, shared_from_this(), std::placeholders::_1))); } -// convenience for ignoring the error code -void -PeerImp::cancelTimer() -{ - try - { - timer_.cancel(); - } - catch (boost::system::system_error const&) - { - // ignored - } -} - -//------------------------------------------------------------------------------ - -std::string -PeerImp::makePrefix(id_t id) -{ - std::stringstream ss; - ss << "[" << std::setfill('0') << std::setw(3) << id << "] "; - return ss.str(); -} - void PeerImp::onTimer(error_code const& ec) { - if (!socket_.is_open()) - return; + XRPL_ASSERT( + strand_.running_in_this_thread(), + "ripple::PeerImp::onTimer : strand in this thread"); - if (ec == boost::asio::error::operation_aborted) + if (!socket_.is_open()) return; if (ec) { + // do not initiate shutdown, timers are frequently cancelled + if (ec == boost::asio::error::operation_aborted) + return; + // This should never happen JLOG(journal_.error()) << "onTimer: " << ec.message(); return close(); } - if (large_sendq_++ >= Tuning::sendqIntervals) + // the timer expired before the shutdown completed + // force close the connection + if (shutdown_) { - fail("Large send queue"); - return; + JLOG(journal_.debug()) << "onTimer: shutdown timer expired"; + return close(); } + if (large_sendq_++ >= Tuning::sendqIntervals) + return fail("Large send queue"); + if (auto const t = tracking_.load(); !inbound_ && t != Tracking::converged) { clock_type::duration duration; @@ -737,17 +777,13 @@ PeerImp::onTimer(error_code const& ec) (duration > app_.config().MAX_UNKNOWN_TIME))) { overlay_.peerFinder().on_failure(slot_); - fail("Not useful"); - return; + return fail("Not useful"); } } // Already waiting for PONG if (lastPingSeq_) - { - fail("Ping Timeout"); - return; - } + return fail("Ping Timeout"); lastPingTime_ = clock_type::now(); lastPingSeq_ = rand_int(); @@ -758,22 +794,28 @@ PeerImp::onTimer(error_code const& ec) send(std::make_shared(message, protocol::mtPING)); - setTimer(); + setTimer(peerTimerInterval); } void -PeerImp::onShutdown(error_code ec) +PeerImp::cancelTimer() noexcept { - cancelTimer(); - // If we don't get eof then something went wrong - if (!ec) + try { - JLOG(journal_.error()) << "onShutdown: expected error condition"; - return close(); + timer_.cancel(); } - if (ec != boost::asio::error::eof) - return fail("onShutdown", ec); - close(); + catch (std::exception const& ex) + { + JLOG(journal_.error()) << "cancelTimer: " << ex.what(); + } +} + +std::string +PeerImp::makePrefix(id_t id) +{ + std::stringstream ss; + ss << "[" << std::setfill('0') << std::setw(3) << id << "] "; + return ss.str(); } //------------------------------------------------------------------------------ @@ -786,6 +828,10 @@ PeerImp::doAccept() JLOG(journal_.debug()) << "doAccept: " << remote_address_; + // a shutdown was initiated before the handshake, there is nothing to do + if (shutdown_) + return tryAsyncShutdown(); + auto const sharedValue = makeSharedValue(*stream_ptr_, journal_); // This shouldn't fail since we already computed @@ -793,7 +839,7 @@ PeerImp::doAccept() if (!sharedValue) return fail("makeSharedValue: Unexpected failure"); - JLOG(journal_.info()) << "Protocol: " << to_string(protocol_); + JLOG(journal_.debug()) << "Protocol: " << to_string(protocol_); JLOG(journal_.info()) << "Public Key: " << toBase58(TokenType::NodePublic, publicKey_); @@ -836,7 +882,7 @@ PeerImp::doAccept() if (!socket_.is_open()) return; if (ec == boost::asio::error::operation_aborted) - return; + return tryAsyncShutdown(); if (ec) return fail("onWriteResponse", ec); if (write_buffer->size() == bytes_transferred) @@ -865,6 +911,10 @@ PeerImp::domain() const void PeerImp::doProtocolStart() { + // a shutdown was initiated before the handshare, there is nothing to do + if (shutdown_) + return tryAsyncShutdown(); + onReadMessage(error_code(), 0); // Send all the validator lists that have been loaded @@ -896,30 +946,45 @@ PeerImp::doProtocolStart() if (auto m = overlay_.getManifestsMessage()) send(m); - setTimer(); + setTimer(peerTimerInterval); } // Called repeatedly with protocol message data void PeerImp::onReadMessage(error_code ec, std::size_t bytes_transferred) { + XRPL_ASSERT( + strand_.running_in_this_thread(), + "ripple::PeerImp::onReadMessage : strand in this thread"); + + readPending_ = false; + if (!socket_.is_open()) return; - if (ec == boost::asio::error::operation_aborted) - return; - if (ec == boost::asio::error::eof) - { - JLOG(journal_.info()) << "EOF"; - return gracefulClose(); - } + if (ec) + { + if (ec == boost::asio::error::eof) + { + JLOG(journal_.debug()) << "EOF"; + return shutdown(); + } + + if (ec == boost::asio::error::operation_aborted) + return tryAsyncShutdown(); + return fail("onReadMessage", ec); + } + // we started shutdown, no reason to process further data + if (shutdown_) + return tryAsyncShutdown(); + if (auto stream = journal_.trace()) { - if (bytes_transferred > 0) - stream << "onReadMessage: " << bytes_transferred << " bytes"; - else - stream << "onReadMessage"; + stream << "onReadMessage: " + << (bytes_transferred > 0 + ? to_string(bytes_transferred) + " bytes" + : ""); } metrics_.recv.add_message(bytes_transferred); @@ -941,17 +1006,29 @@ PeerImp::onReadMessage(error_code ec, std::size_t bytes_transferred) 350ms, journal_); - if (ec) - return fail("onReadMessage", ec); if (!socket_.is_open()) return; - if (gracefulClose_) - return; + + // the error_code is produced by invokeProtocolMessage + // it could be due to a bad message + if (ec) + return fail("onReadMessage", ec); + if (bytes_consumed == 0) break; + read_buffer_.consume(bytes_consumed); } + // check if a shutdown was initiated while processing messages + if (shutdown_) + return tryAsyncShutdown(); + + readPending_ = true; + + XRPL_ASSERT( + !shutdownStarted_, "ripple::PeerImp::onReadMessage : shutdown started"); + // Timeout on writes only stream_.async_read_some( read_buffer_.prepare(std::max(Tuning::readBufferBytes, hint)), @@ -967,18 +1044,29 @@ PeerImp::onReadMessage(error_code ec, std::size_t bytes_transferred) void PeerImp::onWriteMessage(error_code ec, std::size_t bytes_transferred) { + XRPL_ASSERT( + strand_.running_in_this_thread(), + "ripple::PeerImp::onWriteMessage : strand in this thread"); + + writePending_ = false; + if (!socket_.is_open()) return; - if (ec == boost::asio::error::operation_aborted) - return; + if (ec) + { + if (ec == boost::asio::error::operation_aborted) + return tryAsyncShutdown(); + return fail("onWriteMessage", ec); + } + if (auto stream = journal_.trace()) { - if (bytes_transferred > 0) - stream << "onWriteMessage: " << bytes_transferred << " bytes"; - else - stream << "onWriteMessage"; + stream << "onWriteMessage: " + << (bytes_transferred > 0 + ? to_string(bytes_transferred) + " bytes" + : ""); } metrics_.sent.add_message(bytes_transferred); @@ -987,8 +1075,17 @@ PeerImp::onWriteMessage(error_code ec, std::size_t bytes_transferred) !send_queue_.empty(), "ripple::PeerImp::onWriteMessage : non-empty send buffer"); send_queue_.pop(); + + if (shutdown_) + return tryAsyncShutdown(); + if (!send_queue_.empty()) { + writePending_ = true; + XRPL_ASSERT( + !shutdownStarted_, + "ripple::PeerImp::onWriteMessage : shutdown started"); + // Timeout on writes only return boost::asio::async_write( stream_, @@ -1002,16 +1099,6 @@ PeerImp::onWriteMessage(error_code ec, std::size_t bytes_transferred) std::placeholders::_1, std::placeholders::_2))); } - - if (gracefulClose_) - { - return stream_.async_shutdown(bind_executor( - strand_, - std::bind( - &PeerImp::onShutdown, - shared_from_this(), - std::placeholders::_1))); - } } //------------------------------------------------------------------------------ @@ -2880,6 +2967,9 @@ PeerImp::checkTransaction( (stx->getFieldU32(sfLastLedgerSequence) < app_.getLedgerMaster().getValidLedgerIndex())) { + JLOG(p_journal_.info()) + << "Marking transaction " << stx->getTransactionID() + << "as BAD because it's expired"; app_.getHashRouter().setFlags( stx->getTransactionID(), HashRouterFlags::BAD); charge(Resource::feeUselessData, "expired tx"); @@ -2936,7 +3026,7 @@ PeerImp::checkTransaction( { if (!validReason.empty()) { - JLOG(p_journal_.trace()) + JLOG(p_journal_.debug()) << "Exception checking transaction: " << validReason; } @@ -2963,7 +3053,7 @@ PeerImp::checkTransaction( { if (!reason.empty()) { - JLOG(p_journal_.trace()) + JLOG(p_journal_.debug()) << "Exception checking transaction: " << reason; } app_.getHashRouter().setFlags( diff --git a/src/xrpld/overlay/detail/PeerImp.h b/src/xrpld/overlay/detail/PeerImp.h index 3d9a0c0b1e..c2221c136d 100644 --- a/src/xrpld/overlay/detail/PeerImp.h +++ b/src/xrpld/overlay/detail/PeerImp.h @@ -40,6 +40,7 @@ #include #include +#include #include #include #include @@ -49,6 +50,68 @@ namespace ripple { struct ValidatorBlobInfo; class SHAMap; +/** + * @class PeerImp + * @brief This class manages established peer-to-peer connections, handles + message exchange, monitors connection health, and graceful shutdown. + * + + * The PeerImp shutdown mechanism is a multi-stage process + * designed to ensure graceful connection termination while handling ongoing + * I/O operations safely. The shutdown can be initiated from multiple points + * and follows a deterministic state machine. + * + * The shutdown process can be triggered from several entry points: + * - **External requests**: `stop()` method called by overlay management + * - **Error conditions**: `fail(error_code)` or `fail(string)` on protocol + * violations + * - **Timer expiration**: Various timeout scenarios (ping timeout, large send + * queue) + * - **Connection health**: Peer tracking divergence or unknown state timeouts + * + * The shutdown follows this progression: + * + * Normal Operation → shutdown() → tryAsyncShutdown() → onShutdown() → close() + * ↓ ↓ ↓ ↓ + * Set shutdown_ SSL graceful Timer cancel Socket close + * Cancel timer shutdown start & cleanup & metrics + * 5s safety timer Set shutdownStarted_ update + * + * Two primary flags coordinate the shutdown process: + * - `shutdown_`: Set when shutdown is requested + * - `shutdownStarted_`: Set when SSL shutdown begins + * + * The shutdown mechanism carefully coordinates with ongoing read/write + * operations: + * + * **Read Operations (`onReadMessage`)**: + * - Checks `shutdown_` flag after processing each message batch + * - If shutdown initiated during processing, calls `tryAsyncShutdown()` + * + * **Write Operations (`onWriteMessage`)**: + * - Checks `shutdown_` flag before queuing new writes + * - Calls `tryAsyncShutdown()` when shutdown flag detected + * + * Multiple timers require coordination during shutdown: + * 1. **Peer Timer**: Regular ping/pong timer cancelled immediately in + * `shutdown()` + * 2. **Shutdown Timer**: 5-second safety timer ensures shutdown completion + * 3. **Operation Cancellation**: All pending async operations are cancelled + * + * The shutdown implements fallback mechanisms: + * - **Graceful Path**: SSL shutdown → Socket close → Cleanup + * - **Forced Path**: If SSL shutdown fails or times out, proceeds to socket + * close + * - **Safety Timer**: 5-second timeout prevents hanging shutdowns + * + * All shutdown operations are serialized through the boost::asio::strand to + * ensure thread safety. The strand guarantees that shutdown state changes + * and I/O operation callbacks are executed sequentially. + * + * @note This class requires careful coordination between async operations, + * timer management, and shutdown procedures to ensure no resource leaks + * or hanging connections in high-throughput networking scenarios. + */ class PeerImp : public Peer, public std::enable_shared_from_this, public OverlayImpl::Child @@ -79,6 +142,8 @@ private: socket_type& socket_; stream_type& stream_; boost::asio::strand strand_; + + // Multi-purpose timer for peer activity monitoring and shutdown safety waitable_timer timer_; // Updated at each stage of the connection process to reflect @@ -95,7 +160,6 @@ private: std::atomic tracking_; clock_type::time_point trackingTime_; - bool detaching_ = false; // Node public key of peer. PublicKey const publicKey_; std::string name_; @@ -175,7 +239,19 @@ private: http_response_type response_; boost::beast::http::fields const& headers_; std::queue> send_queue_; - bool gracefulClose_ = false; + + // Primary shutdown flag set when shutdown is requested + bool shutdown_ = false; + + // SSL shutdown coordination flag + bool shutdownStarted_ = false; + + // Indicates a read operation is currently pending + bool readPending_ = false; + + // Indicates a write operation is currently pending + bool writePending_ = false; + int large_sendq_ = 0; std::unique_ptr load_event_; // The highest sequence of each PublisherList that has @@ -425,9 +501,6 @@ public: bool isHighLatency() const override; - void - fail(std::string const& reason); - bool compressionEnabled() const override { @@ -441,32 +514,129 @@ public: } private: - void - close(); - + /** + * @brief Handles a failure associated with a specific error code. + * + * This function is called when an operation fails with an error code. It + * logs the warning message and gracefully shutdowns the connection. + * + * The function will do nothing if the connection is already closed or if a + * shutdown is already in progress. + * + * @param name The name of the operation that failed (e.g., "read", + * "write"). + * @param ec The error code associated with the failure. + * @note This function must be called from within the object's strand. + */ void fail(std::string const& name, error_code ec); + /** + * @brief Handles a failure described by a reason string. + * + * This overload is used for logical errors or protocol violations not + * associated with a specific error code. It logs a warning with the + * given reason, then initiates a graceful shutdown. + * + * The function will do nothing if the connection is already closed or if a + * shutdown is already in progress. + * + * @param reason A descriptive string explaining the reason for the failure. + * @note This function must be called from within the object's strand. + */ void - gracefulClose(); + fail(std::string const& reason); + /** @brief Initiates the peer disconnection sequence. + * + * This is the primary entry point to start closing a peer connection. It + * marks the peer for shutdown and cancels any outstanding asynchronous + * operations. This cancellation allows the graceful shutdown to proceed + * once the handlers for the cancelled operations have completed. + * + * @note This method must be called on the peer's strand. + */ void - setTimer(); + shutdown(); + /** @brief Attempts to perform a graceful SSL shutdown if conditions are + * met. + * + * This helper function checks if the peer is in a state where a graceful + * SSL shutdown can be performed (i.e., shutdown has been requested and no + * I/O operations are currently in progress). + * + * @note This method must be called on the peer's strand. + */ void - cancelTimer(); + tryAsyncShutdown(); + + /** + * @brief Handles the completion of the asynchronous SSL shutdown. + * + * This function is the callback for the `async_shutdown` operation started + * in `shutdown()`. Its first action is to cancel the timer. It + * then inspects the error code to determine the outcome. + * + * Regardless of the result, this function proceeds to call `close()` to + * ensure the underlying socket is fully closed. + * + * @param ec The error code resulting from the `async_shutdown` operation. + */ + void + onShutdown(error_code ec); + + /** + * @brief Forcibly closes the underlying socket connection. + * + * This function provides the final, non-graceful shutdown of the peer + * connection. It ensures any pending timers are cancelled and then + * immediately closes the TCP socket, bypassing the SSL shutdown handshake. + * + * After closing, it notifies the overlay manager of the disconnection. + * + * @note This function must be called from within the object's strand. + */ + void + close(); + + /** + * @brief Sets and starts the peer timer. + * + * This function starts timer, which is used to detect inactivity + * and prevent stalled connections. It sets the timer to expire after the + * predefined `peerTimerInterval`. + * + * @note This function will terminate the connection in case of any errors. + */ + void + setTimer(std::chrono::seconds interval); + + /** + * @brief Handles the expiration of the peer activity timer. + * + * This callback is invoked when the timer set by `setTimer` expires. It + * watches the peer connection, checking for various timeout and health + * conditions. + * + * @param ec The error code associated with the timer's expiration. + * `operation_aborted` is expected if the timer was cancelled. + */ + void + onTimer(error_code const& ec); + + /** + * @brief Cancels any pending wait on the peer activity timer. + * + * This function is called to stop the timer. It gracefully manages any + * errors that might occur during the cancellation process. + */ + void + cancelTimer() noexcept; static std::string makePrefix(id_t id); - // Called when the timer wait completes - void - onTimer(boost::system::error_code const& ec); - - // Called when SSL shutdown completes - void - onShutdown(error_code ec); - void doAccept(); diff --git a/src/xrpld/rpc/detail/RPCHelpers.cpp b/src/xrpld/rpc/detail/RPCHelpers.cpp index 52a69eb79e..edb91611be 100644 --- a/src/xrpld/rpc/detail/RPCHelpers.cpp +++ b/src/xrpld/rpc/detail/RPCHelpers.cpp @@ -24,11 +24,11 @@ #include #include #include -#include #include #include #include +#include #include #include #include @@ -941,7 +941,7 @@ chooseLedgerEntryType(Json::Value const& params) #pragma push_macro("LEDGER_ENTRY") #undef LEDGER_ENTRY -#define LEDGER_ENTRY(tag, value, name, rpcName, fields) \ +#define LEDGER_ENTRY(tag, value, name, rpcName, ...) \ {jss::name, jss::rpcName, tag}, #include diff --git a/src/xrpld/rpc/handlers/AMMInfo.cpp b/src/xrpld/rpc/handlers/AMMInfo.cpp index b312264f90..7c1dd3ba58 100644 --- a/src/xrpld/rpc/handlers/AMMInfo.cpp +++ b/src/xrpld/rpc/handlers/AMMInfo.cpp @@ -19,11 +19,11 @@ #include #include -#include #include #include #include +#include #include #include diff --git a/src/xrpld/rpc/handlers/AccountChannels.cpp b/src/xrpld/rpc/handlers/AccountChannels.cpp index 5d810d61a0..1b0046ab64 100644 --- a/src/xrpld/rpc/handlers/AccountChannels.cpp +++ b/src/xrpld/rpc/handlers/AccountChannels.cpp @@ -17,12 +17,12 @@ */ //============================================================================== -#include -#include #include #include #include +#include +#include #include #include #include diff --git a/src/xrpld/rpc/handlers/AccountCurrenciesHandler.cpp b/src/xrpld/rpc/handlers/AccountCurrenciesHandler.cpp index 2d08561ec9..59a13f5893 100644 --- a/src/xrpld/rpc/handlers/AccountCurrenciesHandler.cpp +++ b/src/xrpld/rpc/handlers/AccountCurrenciesHandler.cpp @@ -18,10 +18,10 @@ //============================================================================== #include -#include #include #include +#include #include #include #include diff --git a/src/xrpld/rpc/handlers/AccountInfo.cpp b/src/xrpld/rpc/handlers/AccountInfo.cpp index 3432021690..0a36993d65 100644 --- a/src/xrpld/rpc/handlers/AccountInfo.cpp +++ b/src/xrpld/rpc/handlers/AccountInfo.cpp @@ -19,12 +19,12 @@ #include #include -#include #include #include #include #include +#include #include #include #include diff --git a/src/xrpld/rpc/handlers/AccountLines.cpp b/src/xrpld/rpc/handlers/AccountLines.cpp index e921eee386..893ca9a190 100644 --- a/src/xrpld/rpc/handlers/AccountLines.cpp +++ b/src/xrpld/rpc/handlers/AccountLines.cpp @@ -18,11 +18,11 @@ //============================================================================== #include -#include #include #include #include +#include #include #include #include diff --git a/src/xrpld/rpc/handlers/AccountObjects.cpp b/src/xrpld/rpc/handlers/AccountObjects.cpp index 2b2496a1dd..acd99c205e 100644 --- a/src/xrpld/rpc/handlers/AccountObjects.cpp +++ b/src/xrpld/rpc/handlers/AccountObjects.cpp @@ -18,11 +18,11 @@ //============================================================================== #include -#include #include #include #include +#include #include #include #include diff --git a/src/xrpld/rpc/handlers/AccountOffers.cpp b/src/xrpld/rpc/handlers/AccountOffers.cpp index bc575d2d86..e65b39b35b 100644 --- a/src/xrpld/rpc/handlers/AccountOffers.cpp +++ b/src/xrpld/rpc/handlers/AccountOffers.cpp @@ -17,13 +17,13 @@ */ //============================================================================== -#include -#include #include #include #include #include +#include +#include #include #include #include diff --git a/src/xrpld/rpc/handlers/AccountTx.cpp b/src/xrpld/rpc/handlers/AccountTx.cpp index d5df40303b..6b1dccdba9 100644 --- a/src/xrpld/rpc/handlers/AccountTx.cpp +++ b/src/xrpld/rpc/handlers/AccountTx.cpp @@ -22,13 +22,13 @@ #include #include #include -#include #include #include #include #include #include +#include #include #include #include diff --git a/src/xrpld/rpc/handlers/BookOffers.cpp b/src/xrpld/rpc/handlers/BookOffers.cpp index df4712209c..6506474163 100644 --- a/src/xrpld/rpc/handlers/BookOffers.cpp +++ b/src/xrpld/rpc/handlers/BookOffers.cpp @@ -19,12 +19,12 @@ #include #include -#include #include #include #include #include +#include #include #include #include diff --git a/src/xrpld/rpc/handlers/DepositAuthorized.cpp b/src/xrpld/rpc/handlers/DepositAuthorized.cpp index 1bb480544d..c184a7f845 100644 --- a/src/xrpld/rpc/handlers/DepositAuthorized.cpp +++ b/src/xrpld/rpc/handlers/DepositAuthorized.cpp @@ -17,11 +17,11 @@ */ //============================================================================== -#include -#include #include #include +#include +#include #include #include #include diff --git a/src/xrpld/rpc/handlers/GatewayBalances.cpp b/src/xrpld/rpc/handlers/GatewayBalances.cpp index ca9e370c81..a4542f682f 100644 --- a/src/xrpld/rpc/handlers/GatewayBalances.cpp +++ b/src/xrpld/rpc/handlers/GatewayBalances.cpp @@ -19,10 +19,10 @@ #include #include -#include #include #include +#include #include #include #include diff --git a/src/xrpld/rpc/handlers/GetAggregatePrice.cpp b/src/xrpld/rpc/handlers/GetAggregatePrice.cpp index 33a88ba78f..2227143d70 100644 --- a/src/xrpld/rpc/handlers/GetAggregatePrice.cpp +++ b/src/xrpld/rpc/handlers/GetAggregatePrice.cpp @@ -19,11 +19,11 @@ #include #include -#include #include #include #include +#include #include #include diff --git a/src/xrpld/rpc/handlers/LedgerData.cpp b/src/xrpld/rpc/handlers/LedgerData.cpp index 7bd50cc1e5..e6c579a4b5 100644 --- a/src/xrpld/rpc/handlers/LedgerData.cpp +++ b/src/xrpld/rpc/handlers/LedgerData.cpp @@ -18,13 +18,13 @@ //============================================================================== #include -#include #include #include #include #include #include +#include #include #include #include diff --git a/src/xrpld/rpc/handlers/LedgerEntry.cpp b/src/xrpld/rpc/handlers/LedgerEntry.cpp index 61a7e2fb2c..cead16c04d 100644 --- a/src/xrpld/rpc/handlers/LedgerEntry.cpp +++ b/src/xrpld/rpc/handlers/LedgerEntry.cpp @@ -17,8 +17,6 @@ */ //============================================================================== -#include -#include #include #include #include @@ -28,6 +26,8 @@ #include #include #include +#include +#include #include #include #include diff --git a/src/xrpld/rpc/handlers/LedgerHandler.h b/src/xrpld/rpc/handlers/LedgerHandler.h index a573589cbc..4cbf984505 100644 --- a/src/xrpld/rpc/handlers/LedgerHandler.h +++ b/src/xrpld/rpc/handlers/LedgerHandler.h @@ -23,7 +23,6 @@ #include #include #include -#include #include #include #include @@ -31,6 +30,7 @@ #include #include +#include #include namespace Json { diff --git a/src/xrpld/rpc/handlers/LedgerHeader.cpp b/src/xrpld/rpc/handlers/LedgerHeader.cpp index 1815c8db56..a407dc4318 100644 --- a/src/xrpld/rpc/handlers/LedgerHeader.cpp +++ b/src/xrpld/rpc/handlers/LedgerHeader.cpp @@ -18,10 +18,10 @@ //============================================================================== #include -#include #include #include +#include #include namespace ripple { diff --git a/src/xrpld/rpc/handlers/NFTOffers.cpp b/src/xrpld/rpc/handlers/NFTOffers.cpp index 52a5c69ab0..598abd2c3f 100644 --- a/src/xrpld/rpc/handlers/NFTOffers.cpp +++ b/src/xrpld/rpc/handlers/NFTOffers.cpp @@ -17,13 +17,13 @@ */ //============================================================================== -#include -#include #include #include #include #include +#include +#include #include #include #include diff --git a/src/xrpld/rpc/handlers/NoRippleCheck.cpp b/src/xrpld/rpc/handlers/NoRippleCheck.cpp index a6007e9eab..abe51aa206 100644 --- a/src/xrpld/rpc/handlers/NoRippleCheck.cpp +++ b/src/xrpld/rpc/handlers/NoRippleCheck.cpp @@ -20,11 +20,11 @@ #include #include #include -#include #include #include #include +#include #include #include #include diff --git a/src/xrpld/rpc/handlers/PayChanClaim.cpp b/src/xrpld/rpc/handlers/PayChanClaim.cpp index b62f5e54e5..6945d2a051 100644 --- a/src/xrpld/rpc/handlers/PayChanClaim.cpp +++ b/src/xrpld/rpc/handlers/PayChanClaim.cpp @@ -18,11 +18,11 @@ //============================================================================== #include -#include #include #include #include +#include #include #include #include diff --git a/src/xrpld/rpc/handlers/Subscribe.cpp b/src/xrpld/rpc/handlers/Subscribe.cpp index 1696754e9c..be08769875 100644 --- a/src/xrpld/rpc/handlers/Subscribe.cpp +++ b/src/xrpld/rpc/handlers/Subscribe.cpp @@ -20,13 +20,13 @@ #include #include #include -#include #include #include #include #include #include +#include #include #include #include diff --git a/src/xrpld/rpc/handlers/TransactionEntry.cpp b/src/xrpld/rpc/handlers/TransactionEntry.cpp index c94e95063b..02ff329354 100644 --- a/src/xrpld/rpc/handlers/TransactionEntry.cpp +++ b/src/xrpld/rpc/handlers/TransactionEntry.cpp @@ -19,10 +19,10 @@ #include #include -#include #include #include +#include #include namespace ripple {