From 65e63ebef3e95c804cae4f1085a6e5f45e4748f5 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Wed, 25 Feb 2026 01:12:16 +0000 Subject: [PATCH 1/9] chore: Update cleanup-workspace to delete old .conan2 dir on macOS (#6412) --- .github/workflows/reusable-build-test-config.yml | 2 +- .github/workflows/upload-conan-deps.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/reusable-build-test-config.yml b/.github/workflows/reusable-build-test-config.yml index 4f52b68b84..6060a208fe 100644 --- a/.github/workflows/reusable-build-test-config.yml +++ b/.github/workflows/reusable-build-test-config.yml @@ -101,7 +101,7 @@ jobs: steps: - name: Cleanup workspace (macOS and Windows) if: ${{ runner.os == 'macOS' || runner.os == 'Windows' }} - uses: XRPLF/actions/cleanup-workspace@cf0433aa74563aead044a1e395610c96d65a37cf + uses: XRPLF/actions/cleanup-workspace@c7d9ce5ebb03c752a354889ecd870cadfc2b1cd4 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/.github/workflows/upload-conan-deps.yml b/.github/workflows/upload-conan-deps.yml index b260c4c4f3..df8aa43a18 100644 --- a/.github/workflows/upload-conan-deps.yml +++ b/.github/workflows/upload-conan-deps.yml @@ -64,7 +64,7 @@ jobs: steps: - name: Cleanup workspace (macOS and Windows) if: ${{ runner.os == 'macOS' || runner.os == 'Windows' }} - uses: XRPLF/actions/cleanup-workspace@cf0433aa74563aead044a1e395610c96d65a37cf + uses: XRPLF/actions/cleanup-workspace@c7d9ce5ebb03c752a354889ecd870cadfc2b1cd4 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 From 3a8a18c2cabe704623517eb3767f50e8a908ad95 Mon Sep 17 00:00:00 2001 From: Bart Date: Wed, 25 Feb 2026 18:23:34 -0500 Subject: [PATCH 2/9] refactor: Use uint256 directly as key instead of void pointer (#6313) This change replaces `void const*` by `uint256 const&` for database fetches. Object hashes are expressed using the `uint256` data type, and are converted to `void *` when calling the `fetch` or `fetchBatch` functions. However, in these fetch functions they are converted back to `uint256`, making the conversion process unnecessary. In a few cases the underlying pointer is needed, but that can then be easy obtained via `[hash variable].data()`. --- include/xrpl/nodestore/Backend.h | 6 +++--- src/libxrpl/nodestore/DatabaseNodeImp.cpp | 12 ++---------- src/libxrpl/nodestore/DatabaseRotatingImp.cpp | 2 +- src/libxrpl/nodestore/backend/MemoryFactory.cpp | 7 +++---- src/libxrpl/nodestore/backend/NuDBFactory.cpp | 12 ++++++------ src/libxrpl/nodestore/backend/NullFactory.cpp | 4 ++-- .../nodestore/backend/RocksDBFactory.cpp | 15 +++++++-------- src/test/nodestore/TestBase.h | 4 ++-- src/test/nodestore/Timing_test.cpp | 17 ++++++++--------- 9 files changed, 34 insertions(+), 45 deletions(-) diff --git a/include/xrpl/nodestore/Backend.h b/include/xrpl/nodestore/Backend.h index 7c3ea57bb8..36fd36ec00 100644 --- a/include/xrpl/nodestore/Backend.h +++ b/include/xrpl/nodestore/Backend.h @@ -77,16 +77,16 @@ public: If the object is not found or an error is encountered, the result will indicate the condition. @note This will be called concurrently. - @param key A pointer to the key data. + @param hash The hash of the object. @param pObject [out] The created object if successful. @return The result of the operation. */ virtual Status - fetch(void const* key, std::shared_ptr* pObject) = 0; + fetch(uint256 const& hash, std::shared_ptr* pObject) = 0; /** Fetch a batch synchronously. */ virtual std::pair>, Status> - fetchBatch(std::vector const& hashes) = 0; + fetchBatch(std::vector const& hashes) = 0; /** Store a single object. Depending on the implementation this may happen immediately diff --git a/src/libxrpl/nodestore/DatabaseNodeImp.cpp b/src/libxrpl/nodestore/DatabaseNodeImp.cpp index 5596cb4853..d1452dba86 100644 --- a/src/libxrpl/nodestore/DatabaseNodeImp.cpp +++ b/src/libxrpl/nodestore/DatabaseNodeImp.cpp @@ -33,7 +33,7 @@ DatabaseNodeImp::fetchNodeObject( try { - status = backend_->fetch(hash.data(), &nodeObject); + status = backend_->fetch(hash, &nodeObject); } catch (std::exception const& e) { @@ -68,18 +68,10 @@ DatabaseNodeImp::fetchBatch(std::vector const& hashes) using namespace std::chrono; auto const before = steady_clock::now(); - std::vector batch{}; - batch.reserve(hashes.size()); - for (size_t i = 0; i < hashes.size(); ++i) - { - auto const& hash = hashes[i]; - batch.push_back(&hash); - } - // Get the node objects that match the hashes from the backend. To protect // against the backends returning fewer or more results than expected, the // container is resized to the number of hashes. - auto results = backend_->fetchBatch(batch).first; + auto results = backend_->fetchBatch(hashes).first; XRPL_ASSERT( results.size() == hashes.size() || results.empty(), "number of output objects either matches number of input hashes or is empty"); diff --git a/src/libxrpl/nodestore/DatabaseRotatingImp.cpp b/src/libxrpl/nodestore/DatabaseRotatingImp.cpp index 26d8c30931..e058fa76ac 100644 --- a/src/libxrpl/nodestore/DatabaseRotatingImp.cpp +++ b/src/libxrpl/nodestore/DatabaseRotatingImp.cpp @@ -105,7 +105,7 @@ DatabaseRotatingImp::fetchNodeObject( std::shared_ptr nodeObject; try { - status = backend->fetch(hash.data(), &nodeObject); + status = backend->fetch(hash, &nodeObject); } catch (std::exception const& e) { diff --git a/src/libxrpl/nodestore/backend/MemoryFactory.cpp b/src/libxrpl/nodestore/backend/MemoryFactory.cpp index 8ac23a0bb6..b11d90610a 100644 --- a/src/libxrpl/nodestore/backend/MemoryFactory.cpp +++ b/src/libxrpl/nodestore/backend/MemoryFactory.cpp @@ -116,10 +116,9 @@ public: //-------------------------------------------------------------------------- Status - fetch(void const* key, std::shared_ptr* pObject) override + fetch(uint256 const& hash, std::shared_ptr* pObject) override { XRPL_ASSERT(db_, "xrpl::NodeStore::MemoryBackend::fetch : non-null database"); - uint256 const hash(uint256::fromVoid(key)); std::lock_guard _(db_->mutex); @@ -134,14 +133,14 @@ public: } std::pair>, Status> - fetchBatch(std::vector const& hashes) override + fetchBatch(std::vector const& hashes) override { std::vector> results; results.reserve(hashes.size()); for (auto const& h : hashes) { std::shared_ptr nObj; - Status status = fetch(h->begin(), &nObj); + Status status = fetch(h, &nObj); if (status != ok) results.push_back({}); else diff --git a/src/libxrpl/nodestore/backend/NuDBFactory.cpp b/src/libxrpl/nodestore/backend/NuDBFactory.cpp index e8efa464af..4d7e7be668 100644 --- a/src/libxrpl/nodestore/backend/NuDBFactory.cpp +++ b/src/libxrpl/nodestore/backend/NuDBFactory.cpp @@ -179,17 +179,17 @@ public: } Status - fetch(void const* key, std::shared_ptr* pno) override + fetch(uint256 const& hash, std::shared_ptr* pno) override { Status status; pno->reset(); nudb::error_code ec; db_.fetch( - key, - [key, pno, &status](void const* data, std::size_t size) { + hash.data(), + [&hash, pno, &status](void const* data, std::size_t size) { nudb::detail::buffer bf; auto const result = nodeobject_decompress(data, size, bf); - DecodedBlob decoded(key, result.first, result.second); + DecodedBlob decoded(hash.data(), result.first, result.second); if (!decoded.wasOk()) { status = dataCorrupt; @@ -207,14 +207,14 @@ public: } std::pair>, Status> - fetchBatch(std::vector const& hashes) override + fetchBatch(std::vector const& hashes) override { std::vector> results; results.reserve(hashes.size()); for (auto const& h : hashes) { std::shared_ptr nObj; - Status status = fetch(h->begin(), &nObj); + Status status = fetch(h, &nObj); if (status != ok) results.push_back({}); else diff --git a/src/libxrpl/nodestore/backend/NullFactory.cpp b/src/libxrpl/nodestore/backend/NullFactory.cpp index 4ecca46a9a..ab5b7d0117 100644 --- a/src/libxrpl/nodestore/backend/NullFactory.cpp +++ b/src/libxrpl/nodestore/backend/NullFactory.cpp @@ -36,13 +36,13 @@ public: } Status - fetch(void const*, std::shared_ptr*) override + fetch(uint256 const&, std::shared_ptr*) override { return notFound; } std::pair>, Status> - fetchBatch(std::vector const& hashes) override + fetchBatch(std::vector const& hashes) override { return {}; } diff --git a/src/libxrpl/nodestore/backend/RocksDBFactory.cpp b/src/libxrpl/nodestore/backend/RocksDBFactory.cpp index c84c5f6982..01bc74f5ed 100644 --- a/src/libxrpl/nodestore/backend/RocksDBFactory.cpp +++ b/src/libxrpl/nodestore/backend/RocksDBFactory.cpp @@ -244,7 +244,7 @@ public: //-------------------------------------------------------------------------- Status - fetch(void const* key, std::shared_ptr* pObject) override + fetch(uint256 const& hash, std::shared_ptr* pObject) override { XRPL_ASSERT(m_db, "xrpl::NodeStore::RocksDBBackend::fetch : non-null database"); pObject->reset(); @@ -252,7 +252,7 @@ public: Status status(ok); rocksdb::ReadOptions const options; - rocksdb::Slice const slice(static_cast(key), m_keyBytes); + rocksdb::Slice const slice(std::bit_cast(hash.data()), m_keyBytes); std::string string; @@ -260,7 +260,7 @@ public: if (getStatus.ok()) { - DecodedBlob decoded(key, string.data(), string.size()); + DecodedBlob decoded(hash.data(), string.data(), string.size()); if (decoded.wasOk()) { @@ -295,14 +295,14 @@ public: } std::pair>, Status> - fetchBatch(std::vector const& hashes) override + fetchBatch(std::vector const& hashes) override { std::vector> results; results.reserve(hashes.size()); for (auto const& h : hashes) { std::shared_ptr nObj; - Status status = fetch(h->begin(), &nObj); + Status status = fetch(h, &nObj); if (status != ok) results.push_back({}); else @@ -332,9 +332,8 @@ public: EncodedBlob encoded(e); wb.Put( - rocksdb::Slice(reinterpret_cast(encoded.getKey()), m_keyBytes), - rocksdb::Slice( - reinterpret_cast(encoded.getData()), encoded.getSize())); + rocksdb::Slice(std::bit_cast(encoded.getKey()), m_keyBytes), + rocksdb::Slice(std::bit_cast(encoded.getData()), encoded.getSize())); } rocksdb::WriteOptions const options; diff --git a/src/test/nodestore/TestBase.h b/src/test/nodestore/TestBase.h index 4a4d21002e..cb2a8e3bd5 100644 --- a/src/test/nodestore/TestBase.h +++ b/src/test/nodestore/TestBase.h @@ -138,7 +138,7 @@ public: { std::shared_ptr object; - Status const status = backend.fetch(batch[i]->getHash().cbegin(), &object); + Status const status = backend.fetch(batch[i]->getHash(), &object); BEAST_EXPECT(status == ok); @@ -158,7 +158,7 @@ public: { std::shared_ptr object; - Status const status = backend.fetch(batch[i]->getHash().cbegin(), &object); + Status const status = backend.fetch(batch[i]->getHash(), &object); BEAST_EXPECT(status == notFound); } diff --git a/src/test/nodestore/Timing_test.cpp b/src/test/nodestore/Timing_test.cpp index dae131e5e7..b537e3abb7 100644 --- a/src/test/nodestore/Timing_test.cpp +++ b/src/test/nodestore/Timing_test.cpp @@ -314,7 +314,7 @@ public: std::shared_ptr obj; std::shared_ptr result; obj = seq1_.obj(dist_(gen_)); - backend_.fetch(obj->getHash().data(), &result); + backend_.fetch(obj->getHash(), &result); suite_.expect(result && isSame(result, obj)); } catch (std::exception const& e) @@ -377,9 +377,9 @@ public: { try { - auto const key = seq2_.key(i); + auto const hash = seq2_.key(i); std::shared_ptr result; - backend_.fetch(key.data(), &result); + backend_.fetch(hash, &result); suite_.expect(!result); } catch (std::exception const& e) @@ -449,9 +449,9 @@ public: { if (rand_(gen_) < missingNodePercent) { - auto const key = seq2_.key(dist_(gen_)); + auto const hash = seq2_.key(dist_(gen_)); std::shared_ptr result; - backend_.fetch(key.data(), &result); + backend_.fetch(hash, &result); suite_.expect(!result); } else @@ -459,7 +459,7 @@ public: std::shared_ptr obj; std::shared_ptr result; obj = seq1_.obj(dist_(gen_)); - backend_.fetch(obj->getHash().data(), &result); + backend_.fetch(obj->getHash(), &result); suite_.expect(result && isSame(result, obj)); } } @@ -540,8 +540,7 @@ public: std::shared_ptr result; auto const j = older_(gen_); obj = seq1_.obj(j); - std::shared_ptr result1; - backend_.fetch(obj->getHash().data(), &result); + backend_.fetch(obj->getHash(), &result); suite_.expect(result != nullptr); suite_.expect(isSame(result, obj)); } @@ -559,7 +558,7 @@ public: std::shared_ptr result; auto const j = recent_(gen_); obj = seq1_.obj(j); - backend_.fetch(obj->getHash().data(), &result); + backend_.fetch(obj->getHash(), &result); suite_.expect(!result || isSame(result, obj)); break; } From 2e595b603118963d209ef81386dfed4a413912c2 Mon Sep 17 00:00:00 2001 From: Alex Kremer Date: Thu, 26 Feb 2026 18:26:58 +0000 Subject: [PATCH 3/9] chore: Enable clang-tidy checks without issues (#6414) This change enables all clang-tidy checks that are already passing. It also modifies the clang-tidy CI job, so it runs against all files if .clang-tidy changed. --- .clang-tidy | 311 +++++++++--------- .../workflows/reusable-clang-tidy-files.yml | 4 +- .github/workflows/reusable-clang-tidy.yml | 14 +- CONTRIBUTING.md | 23 ++ 4 files changed, 196 insertions(+), 156 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index f7009c4666..5f4187b008 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,105 +1,143 @@ --- Checks: "-*, - bugprone-argument-comment + bugprone-argument-comment, + bugprone-assert-side-effect, + bugprone-bad-signal-to-kill-thread, + bugprone-bool-pointer-implicit-conversion, + bugprone-casting-through-void, + bugprone-chained-comparison, + bugprone-compare-pointer-to-member-virtual-function, + bugprone-copy-constructor-init, + bugprone-dangling-handle, + bugprone-dynamic-static-initializers, + bugprone-fold-init-type, + bugprone-forward-declaration-namespace, + bugprone-inaccurate-erase, + bugprone-incorrect-enable-if, + bugprone-incorrect-roundings, + bugprone-infinite-loop, + bugprone-integer-division, + bugprone-lambda-function-name, + bugprone-macro-parentheses, + bugprone-macro-repeated-side-effects, + bugprone-misplaced-operator-in-strlen-in-alloc, + bugprone-misplaced-pointer-arithmetic-in-alloc, + bugprone-misplaced-widening-cast, + bugprone-multi-level-implicit-pointer-conversion, + bugprone-multiple-new-in-one-expression, + bugprone-multiple-statement-macro, + bugprone-no-escape, + bugprone-non-zero-enum-to-bool-conversion, + bugprone-parent-virtual-call, + bugprone-posix-return, + bugprone-redundant-branch-condition, + bugprone-shared-ptr-array-mismatch, + bugprone-signal-handler, + bugprone-signed-char-misuse, + bugprone-sizeof-container, + bugprone-spuriously-wake-up-functions, + bugprone-standalone-empty, + bugprone-string-constructor, + bugprone-string-integer-assignment, + bugprone-string-literal-with-embedded-nul, + bugprone-stringview-nullptr, + bugprone-suspicious-enum-usage, + bugprone-suspicious-include, + bugprone-suspicious-memory-comparison, + bugprone-suspicious-memset-usage, + bugprone-suspicious-realloc-usage, + bugprone-suspicious-semicolon, + bugprone-suspicious-string-compare, + bugprone-swapped-arguments, + bugprone-terminating-continue, + bugprone-throw-keyword-missing, + bugprone-undefined-memory-manipulation, + bugprone-undelegated-constructor, + bugprone-unhandled-exception-at-new, + bugprone-unique-ptr-array-mismatch, + bugprone-unsafe-functions, + bugprone-virtual-near-miss, + cppcoreguidelines-no-suspend-with-lock, + cppcoreguidelines-virtual-class-destructor, + hicpp-ignored-remove-result, + misc-definitions-in-headers, + misc-header-include-cycle, + misc-misplaced-const, + misc-static-assert, + misc-throw-by-value-catch-by-reference, + misc-unused-alias-decls, + misc-unused-using-decls, + readability-duplicate-include, + readability-enum-initial-value, + readability-misleading-indentation, + readability-non-const-parameter, + readability-redundant-declaration, + readability-reference-to-constructed-temporary, + modernize-deprecated-headers, + modernize-make-shared, + modernize-make-unique, + performance-implicit-conversion-in-loop, + performance-move-constructor-init, + performance-trivially-destructible " -# bugprone-assert-side-effect, -# bugprone-bad-signal-to-kill-thread, -# bugprone-bool-pointer-implicit-conversion, -# bugprone-casting-through-void, -# bugprone-chained-comparison, -# bugprone-compare-pointer-to-member-virtual-function, -# bugprone-copy-constructor-init, -# bugprone-crtp-constructor-accessibility, -# bugprone-dangling-handle, -# bugprone-dynamic-static-initializers, +# --- +# checks that have some issues that need to be resolved: +# # bugprone-empty-catch, -# bugprone-fold-init-type, -# bugprone-forward-declaration-namespace, -# bugprone-inaccurate-erase, +# bugprone-crtp-constructor-accessibility, # bugprone-inc-dec-in-conditions, -# bugprone-incorrect-enable-if, -# bugprone-incorrect-roundings, -# bugprone-infinite-loop, -# bugprone-integer-division, -# bugprone-lambda-function-name, -# bugprone-macro-parentheses, -# bugprone-macro-repeated-side-effects, -# bugprone-misplaced-operator-in-strlen-in-alloc, -# bugprone-misplaced-pointer-arithmetic-in-alloc, -# bugprone-misplaced-widening-cast, -# bugprone-move-forwarding-reference, -# bugprone-multi-level-implicit-pointer-conversion, -# bugprone-multiple-new-in-one-expression, -# bugprone-multiple-statement-macro, -# bugprone-no-escape, -# bugprone-non-zero-enum-to-bool-conversion, -# bugprone-optional-value-conversion, -# bugprone-parent-virtual-call, -# bugprone-pointer-arithmetic-on-polymorphic-object, -# bugprone-posix-return, -# bugprone-redundant-branch-condition, # bugprone-reserved-identifier, -# bugprone-return-const-ref-from-parameter, -# bugprone-shared-ptr-array-mismatch, -# bugprone-signal-handler, -# bugprone-signed-char-misuse, -# bugprone-sizeof-container, -# bugprone-sizeof-expression, -# bugprone-spuriously-wake-up-functions, -# bugprone-standalone-empty, -# bugprone-string-constructor, -# bugprone-string-integer-assignment, -# bugprone-string-literal-with-embedded-nul, -# bugprone-stringview-nullptr, -# bugprone-suspicious-enum-usage, -# bugprone-suspicious-include, -# bugprone-suspicious-memory-comparison, -# bugprone-suspicious-memset-usage, -# bugprone-suspicious-missing-comma, -# bugprone-suspicious-realloc-usage, -# bugprone-suspicious-semicolon, -# bugprone-suspicious-string-compare, -# bugprone-suspicious-stringview-data-usage, -# bugprone-swapped-arguments, -# bugprone-switch-missing-default-case, -# bugprone-terminating-continue, -# bugprone-throw-keyword-missing, -# bugprone-too-small-loop-variable, -# bugprone-undefined-memory-manipulation, -# bugprone-undelegated-constructor, -# bugprone-unhandled-exception-at-new, -# bugprone-unhandled-self-assignment, -# bugprone-unique-ptr-array-mismatch, -# bugprone-unsafe-functions, +# bugprone-move-forwarding-reference, # bugprone-unused-local-non-trivial-variable, -# bugprone-unused-raii, +# bugprone-return-const-ref-from-parameter, +# bugprone-switch-missing-default-case, +# bugprone-sizeof-expression, +# bugprone-suspicious-stringview-data-usage, +# bugprone-suspicious-missing-comma, +# bugprone-pointer-arithmetic-on-polymorphic-object, +# bugprone-optional-value-conversion, +# bugprone-too-small-loop-variable, # bugprone-unused-return-value, # bugprone-use-after-move, -# bugprone-virtual-near-miss, -# cppcoreguidelines-init-variables, +# bugprone-unhandled-self-assignment, +# bugprone-unused-raii, +# # cppcoreguidelines-misleading-capture-default-by-value, -# cppcoreguidelines-no-suspend-with-lock, +# cppcoreguidelines-init-variables, # cppcoreguidelines-pro-type-member-init, # cppcoreguidelines-pro-type-static-cast-downcast, -# cppcoreguidelines-rvalue-reference-param-not-moved, # cppcoreguidelines-use-default-member-init, -# cppcoreguidelines-virtual-class-destructor, -# hicpp-ignored-remove-result, +# cppcoreguidelines-rvalue-reference-param-not-moved, +# # llvm-namespace-comment, # misc-const-correctness, -# misc-definitions-in-headers, -# misc-header-include-cycle, # misc-include-cleaner, -# misc-misplaced-const, # misc-redundant-expression, -# misc-static-assert, -# misc-throw-by-value-catch-by-reference, -# misc-unused-alias-decls, -# misc-unused-using-decls, +# +# readability-avoid-nested-conditional-operator, +# readability-avoid-return-with-void-value, +# readability-braces-around-statements, +# readability-container-contains, +# readability-container-size-empty, +# readability-convert-member-functions-to-static, +# readability-const-return-type, +# readability-else-after-return, +# readability-implicit-bool-conversion, +# readability-inconsistent-declaration-parameter-name, +# readability-identifier-naming, +# readability-make-member-function-const, +# readability-math-missing-parentheses, +# readability-redundant-inline-specifier, +# readability-redundant-member-init, +# readability-redundant-casting, +# readability-redundant-string-init, +# readability-simplify-boolean-expr, +# readability-static-definition-in-anonymous-namespace, +# readability-suspicious-call-argument, +# readability-use-std-min-max, +# readability-static-accessed-through-instance, +# # modernize-concat-nested-namespaces, -# modernize-deprecated-headers, -# modernize-make-shared, -# modernize-make-unique, # modernize-pass-by-value, # modernize-type-traits, # modernize-use-designated-initializers, @@ -111,79 +149,50 @@ Checks: "-*, # modernize-use-starts-ends-with, # modernize-use-std-numbers, # modernize-use-using, +# # performance-faster-string-find, # performance-for-range-copy, -# performance-implicit-conversion-in-loop, # performance-inefficient-vector-operation, # performance-move-const-arg, -# performance-move-constructor-init, # performance-no-automatic-move, -# performance-trivially-destructible, -# readability-avoid-nested-conditional-operator, -# readability-avoid-return-with-void-value, -# readability-braces-around-statements, -# readability-const-return-type, -# readability-container-contains, -# readability-container-size-empty, -# readability-convert-member-functions-to-static, -# readability-duplicate-include, -# readability-else-after-return, -# readability-enum-initial-value, -# readability-implicit-bool-conversion, -# readability-inconsistent-declaration-parameter-name, -# readability-identifier-naming, -# readability-make-member-function-const, -# readability-math-missing-parentheses, -# readability-misleading-indentation, -# readability-non-const-parameter, -# readability-redundant-casting, -# readability-redundant-declaration, -# readability-redundant-inline-specifier, -# readability-redundant-member-init, -# readability-redundant-string-init, -# readability-reference-to-constructed-temporary, -# readability-simplify-boolean-expr, -# readability-static-accessed-through-instance, -# readability-static-definition-in-anonymous-namespace, -# readability-suspicious-call-argument, -# readability-use-std-min-max +# --- # -# CheckOptions: -# readability-braces-around-statements.ShortStatementLines: 2 -# readability-identifier-naming.MacroDefinitionCase: UPPER_CASE -# readability-identifier-naming.ClassCase: CamelCase -# readability-identifier-naming.StructCase: CamelCase -# readability-identifier-naming.UnionCase: CamelCase -# readability-identifier-naming.EnumCase: CamelCase -# readability-identifier-naming.EnumConstantCase: CamelCase -# readability-identifier-naming.ScopedEnumConstantCase: CamelCase -# readability-identifier-naming.GlobalConstantCase: UPPER_CASE -# readability-identifier-naming.GlobalConstantPrefix: "k" -# readability-identifier-naming.GlobalVariableCase: CamelCase -# readability-identifier-naming.GlobalVariablePrefix: "g" -# readability-identifier-naming.ConstexprFunctionCase: camelBack -# readability-identifier-naming.ConstexprMethodCase: camelBack -# readability-identifier-naming.ClassMethodCase: camelBack -# readability-identifier-naming.ClassMemberCase: camelBack -# readability-identifier-naming.ClassConstantCase: UPPER_CASE -# readability-identifier-naming.ClassConstantPrefix: "k" -# readability-identifier-naming.StaticConstantCase: UPPER_CASE -# readability-identifier-naming.StaticConstantPrefix: "k" -# readability-identifier-naming.StaticVariableCase: UPPER_CASE -# readability-identifier-naming.StaticVariablePrefix: "k" -# readability-identifier-naming.ConstexprVariableCase: UPPER_CASE -# readability-identifier-naming.ConstexprVariablePrefix: "k" -# readability-identifier-naming.LocalConstantCase: camelBack -# readability-identifier-naming.LocalVariableCase: camelBack -# readability-identifier-naming.TemplateParameterCase: CamelCase -# readability-identifier-naming.ParameterCase: camelBack -# readability-identifier-naming.FunctionCase: camelBack -# readability-identifier-naming.MemberCase: camelBack -# readability-identifier-naming.PrivateMemberSuffix: _ -# readability-identifier-naming.ProtectedMemberSuffix: _ -# readability-identifier-naming.PublicMemberSuffix: "" -# readability-identifier-naming.FunctionIgnoredRegexp: ".*tag_invoke.*" -# bugprone-unsafe-functions.ReportMoreUnsafeFunctions: true +CheckOptions: + # readability-braces-around-statements.ShortStatementLines: 2 + # readability-identifier-naming.MacroDefinitionCase: UPPER_CASE + # readability-identifier-naming.ClassCase: CamelCase + # readability-identifier-naming.StructCase: CamelCase + # readability-identifier-naming.UnionCase: CamelCase + # readability-identifier-naming.EnumCase: CamelCase + # readability-identifier-naming.EnumConstantCase: CamelCase + # readability-identifier-naming.ScopedEnumConstantCase: CamelCase + # readability-identifier-naming.GlobalConstantCase: UPPER_CASE + # readability-identifier-naming.GlobalConstantPrefix: "k" + # readability-identifier-naming.GlobalVariableCase: CamelCase + # readability-identifier-naming.GlobalVariablePrefix: "g" + # readability-identifier-naming.ConstexprFunctionCase: camelBack + # readability-identifier-naming.ConstexprMethodCase: camelBack + # readability-identifier-naming.ClassMethodCase: camelBack + # readability-identifier-naming.ClassMemberCase: camelBack + # readability-identifier-naming.ClassConstantCase: UPPER_CASE + # readability-identifier-naming.ClassConstantPrefix: "k" + # readability-identifier-naming.StaticConstantCase: UPPER_CASE + # readability-identifier-naming.StaticConstantPrefix: "k" + # readability-identifier-naming.StaticVariableCase: UPPER_CASE + # readability-identifier-naming.StaticVariablePrefix: "k" + # readability-identifier-naming.ConstexprVariableCase: UPPER_CASE + # readability-identifier-naming.ConstexprVariablePrefix: "k" + # readability-identifier-naming.LocalConstantCase: camelBack + # readability-identifier-naming.LocalVariableCase: camelBack + # readability-identifier-naming.TemplateParameterCase: CamelCase + # readability-identifier-naming.ParameterCase: camelBack + # readability-identifier-naming.FunctionCase: camelBack + # readability-identifier-naming.MemberCase: camelBack + # readability-identifier-naming.PrivateMemberSuffix: _ + # readability-identifier-naming.ProtectedMemberSuffix: _ + # readability-identifier-naming.PublicMemberSuffix: "" + # readability-identifier-naming.FunctionIgnoredRegexp: ".*tag_invoke.*" + bugprone-unsafe-functions.ReportMoreUnsafeFunctions: true # bugprone-unused-return-value.CheckedReturnTypes: ::std::error_code;::std::error_condition;::std::errc # misc-include-cleaner.IgnoreHeaders: '.*/(detail|impl)/.*;.*(expected|unexpected).*;.*ranges_lower_bound\.h;time.h;stdlib.h;__chrono/.*;fmt/chrono.h;boost/uuid/uuid_hash.hpp' # diff --git a/.github/workflows/reusable-clang-tidy-files.yml b/.github/workflows/reusable-clang-tidy-files.yml index 432da1d15c..d36dea747c 100644 --- a/.github/workflows/reusable-clang-tidy-files.yml +++ b/.github/workflows/reusable-clang-tidy-files.yml @@ -78,9 +78,9 @@ jobs: id: run_clang_tidy continue-on-error: true env: - FILES: ${{ inputs.files }} + TARGETS: ${{ inputs.files != '' && inputs.files || 'src tests' }} run: | - run-clang-tidy -j ${{ steps.nproc.outputs.nproc }} -p "$BUILD_DIR" $FILES 2>&1 | tee clang-tidy-output.txt + run-clang-tidy -j ${{ steps.nproc.outputs.nproc }} -p "${BUILD_DIR}" ${TARGETS} 2>&1 | tee clang-tidy-output.txt - name: Upload clang-tidy output if: steps.run_clang_tidy.outcome != 'success' diff --git a/.github/workflows/reusable-clang-tidy.yml b/.github/workflows/reusable-clang-tidy.yml index 7c300ee26e..7050d3509f 100644 --- a/.github/workflows/reusable-clang-tidy.yml +++ b/.github/workflows/reusable-clang-tidy.yml @@ -22,7 +22,8 @@ jobs: if: ${{ inputs.check_only_changed }} runs-on: ubuntu-latest outputs: - any_changed: ${{ steps.changed_files.outputs.any_changed }} + clang_tidy_config_changed: ${{ steps.changed_clang_tidy.outputs.any_changed }} + any_cpp_changed: ${{ steps.changed_files.outputs.any_changed }} all_changed_files: ${{ steps.changed_files.outputs.all_changed_files }} steps: - name: Checkout repository @@ -38,10 +39,17 @@ jobs: **/*.ipp separator: " " + - name: Get changed clang-tidy configuration + id: changed_clang_tidy + uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 + with: + files: | + .clang-tidy + run-clang-tidy: needs: [determine-files] - if: ${{ always() && !cancelled() && (!inputs.check_only_changed || needs.determine-files.outputs.any_changed == 'true') }} + if: ${{ always() && !cancelled() && (!inputs.check_only_changed || needs.determine-files.outputs.any_cpp_changed == 'true' || needs.determine-files.outputs.clang_tidy_config_changed == 'true') }} uses: ./.github/workflows/reusable-clang-tidy-files.yml with: - files: ${{ inputs.check_only_changed && needs.determine-files.outputs.all_changed_files || '' }} + files: ${{ (needs.determine-files.outputs.clang_tidy_config_changed == 'true' && '') || (inputs.check_only_changed && needs.determine-files.outputs.all_changed_files || '') }} create_issue_on_failure: ${{ inputs.create_issue_on_failure }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a928065ef2..4bb1db8689 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -251,6 +251,29 @@ pip3 install pre-commit pre-commit install ``` +## Clang-tidy + +All code must pass `clang-tidy` checks according to the settings in [`.clang-tidy`](./.clang-tidy). + +There is a Continuous Integration job that runs clang-tidy on pull requests. The CI will check: + +- All changed C++ files (`.cpp`, `.h`, `.ipp`) when only code files are modified +- **All files in the repository** when the `.clang-tidy` configuration file is changed + +This ensures that configuration changes don't introduce new warnings across the codebase. + +### Running clang-tidy locally + +Before running clang-tidy, you must build the project to generate required files (particularly protobuf headers). Refer to [`BUILD.md`](./BUILD.md) for build instructions. + +Then run clang-tidy on your local changes: + +``` +run-clang-tidy -p build src tests +``` + +This will check all source files in the `src` and `tests` directories using the compile commands from your `build` directory. + ## Contracts and instrumentation We are using [Antithesis](https://antithesis.com/) for continuous fuzzing, From 404f35d5568f60d914b9f2749fff9518cd1324cb Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Thu, 26 Feb 2026 22:01:38 -0500 Subject: [PATCH 4/9] test: Grep for failures in CI (#6339) This change adjusts the CI tests to make it easier to spot errors, without needing to sift through the thousands of lines of output. --- .github/workflows/reusable-build-test-config.yml | 15 ++++++++++++++- src/test/app/Vault_test.cpp | 8 ++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/.github/workflows/reusable-build-test-config.yml b/.github/workflows/reusable-build-test-config.yml index 6060a208fe..dabcc737f8 100644 --- a/.github/workflows/reusable-build-test-config.yml +++ b/.github/workflows/reusable-build-test-config.yml @@ -229,8 +229,21 @@ jobs: env: BUILD_NPROC: ${{ steps.nproc.outputs.nproc }} run: | - ./xrpld --unittest --unittest-jobs "${BUILD_NPROC}" + set -o pipefail + ./xrpld --unittest --unittest-jobs "${BUILD_NPROC}" 2>&1 | tee unittest.log + - name: Show test failure summary + if: ${{ failure() && !inputs.build_only }} + working-directory: ${{ runner.os == 'Windows' && format('{0}/{1}', env.BUILD_DIR, inputs.build_type) || env.BUILD_DIR }} + run: | + if [ ! -f unittest.log ]; then + echo "unittest.log not found; embedded tests may not have run." + exit 0 + fi + + if ! grep -E "failed" unittest.log; then + echo "Log present but no failure lines found in unittest.log." + fi - name: Debug failure (Linux) if: ${{ failure() && runner.os == 'Linux' && !inputs.build_only }} run: | diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 93ac94d7ce..7ae9faf18f 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -5340,20 +5340,20 @@ class Vault_test : public beast::unit_test::suite env.close(); // 2. Mantissa larger than uint64 max + env.set_parse_failure_expected(true); try { tx[sfAssetsMaximum] = "18446744073709551617e5"; // uint64 max + 1 env(tx, THISLINE); - BEAST_EXPECT(false); + BEAST_EXPECTS(false, "Expected parse_error for mantissa larger than uint64 max"); } catch (parse_error const& e) { using namespace std::string_literals; BEAST_EXPECT( - e.what() == - "invalidParamsField 'tx_json.AssetsMaximum' has invalid " - "data."s); + e.what() == "invalidParamsField 'tx_json.AssetsMaximum' has invalid data."s); } + env.set_parse_failure_expected(false); } } From b58c681189d361fa3bca9f537d1e732aa2369467 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Fri, 27 Feb 2026 18:36:10 +0000 Subject: [PATCH 5/9] chore: Make nix hook optional (#6431) This change makes the `nix` pre-commit hook optional in development environments, and enforced only inside Github Actions. --- .github/workflows/pre-commit.yml | 2 +- .pre-commit-config.yaml | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 7793d1e3ab..54a84a426a 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -11,7 +11,7 @@ on: jobs: # Call the workflow in the XRPLF/actions repo that runs the pre-commit hooks. run-hooks: - uses: XRPLF/actions/.github/workflows/pre-commit.yml@320be44621ca2a080f05aeb15817c44b84518108 + uses: XRPLF/actions/.github/workflows/pre-commit.yml@56de1bdf19639e009639a50b8d17c28ca954f267 with: runs_on: ubuntu-latest container: '{ "image": "ghcr.io/xrplf/ci/tools-rippled-pre-commit:sha-41ec7c1" }' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6e04c752e9..c17eb92787 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -61,7 +61,15 @@ repos: hooks: - id: nix-fmt name: Format Nix files - entry: nix --extra-experimental-features 'nix-command flakes' fmt + entry: | + bash -c ' + if command -v nix &> /dev/null || [ "$GITHUB_ACTIONS" = "true" ]; then + nix --extra-experimental-features "nix-command flakes" fmt "$@" + else + echo "Skipping nix-fmt: nix not installed and not in GitHub Actions" + exit 0 + fi + ' -- language: system types: - nix From 1a7f824b8944b3cf92cd86a8f6049337a6fac6c3 Mon Sep 17 00:00:00 2001 From: Vito Tumas <5780819+Tapanito@users.noreply.github.com> Date: Fri, 27 Feb 2026 22:02:39 +0100 Subject: [PATCH 6/9] refactor: Splits invariant checks into multiple classes (#6440) The invariant check system had grown into a single monolithic file pair containing 24 invariant checker classes. The large `InvariantCheck.cpp` file was a frequent source of merge conflicts and difficult to navigate. This refactoring improves maintainability and readability with zero behavioral changes. In particular, this change: - Splits `InvariantCheck.h` and `InvariantCheck.cpp` into 10 focused header/source pairs organized by domain under a new `invariants/` subdirectory. - Extracts the shared `Privilege` enum and `hasPrivilege()` function into a dedicated `InvariantCheckPrivilege.h` header, so domain-specific files can reference them independently. --- include/xrpl/tx/InvariantCheck.h | 732 ---- include/xrpl/tx/invariants/AMMInvariant.h | 53 + include/xrpl/tx/invariants/FreezeInvariant.h | 84 + include/xrpl/tx/invariants/InvariantCheck.h | 385 ++ .../tx/invariants/InvariantCheckPrivilege.h | 60 + include/xrpl/tx/invariants/LoanInvariant.h | 75 + include/xrpl/tx/invariants/MPTInvariant.h | 31 + include/xrpl/tx/invariants/NFTInvariant.h | 70 + .../tx/invariants/PermissionedDEXInvariant.h | 25 + .../invariants/PermissionedDomainInvariant.h | 41 + include/xrpl/tx/invariants/VaultInvariant.h | 77 + src/libxrpl/tx/ApplyContext.cpp | 5 +- src/libxrpl/tx/InvariantCheck.cpp | 3483 ----------------- src/libxrpl/tx/invariants/AMMInvariant.cpp | 305 ++ src/libxrpl/tx/invariants/FreezeInvariant.cpp | 278 ++ src/libxrpl/tx/invariants/InvariantCheck.cpp | 1009 +++++ src/libxrpl/tx/invariants/LoanInvariant.cpp | 278 ++ src/libxrpl/tx/invariants/MPTInvariant.cpp | 192 + src/libxrpl/tx/invariants/NFTInvariant.cpp | 274 ++ .../invariants/PermissionedDEXInvariant.cpp | 93 + .../PermissionedDomainInvariant.cpp | 162 + src/libxrpl/tx/invariants/VaultInvariant.cpp | 926 +++++ .../PermissionedDomainSet.cpp | 5 +- 23 files changed, 4423 insertions(+), 4220 deletions(-) delete mode 100644 include/xrpl/tx/InvariantCheck.h create mode 100644 include/xrpl/tx/invariants/AMMInvariant.h create mode 100644 include/xrpl/tx/invariants/FreezeInvariant.h create mode 100644 include/xrpl/tx/invariants/InvariantCheck.h create mode 100644 include/xrpl/tx/invariants/InvariantCheckPrivilege.h create mode 100644 include/xrpl/tx/invariants/LoanInvariant.h create mode 100644 include/xrpl/tx/invariants/MPTInvariant.h create mode 100644 include/xrpl/tx/invariants/NFTInvariant.h create mode 100644 include/xrpl/tx/invariants/PermissionedDEXInvariant.h create mode 100644 include/xrpl/tx/invariants/PermissionedDomainInvariant.h create mode 100644 include/xrpl/tx/invariants/VaultInvariant.h delete mode 100644 src/libxrpl/tx/InvariantCheck.cpp create mode 100644 src/libxrpl/tx/invariants/AMMInvariant.cpp create mode 100644 src/libxrpl/tx/invariants/FreezeInvariant.cpp create mode 100644 src/libxrpl/tx/invariants/InvariantCheck.cpp create mode 100644 src/libxrpl/tx/invariants/LoanInvariant.cpp create mode 100644 src/libxrpl/tx/invariants/MPTInvariant.cpp create mode 100644 src/libxrpl/tx/invariants/NFTInvariant.cpp create mode 100644 src/libxrpl/tx/invariants/PermissionedDEXInvariant.cpp create mode 100644 src/libxrpl/tx/invariants/PermissionedDomainInvariant.cpp create mode 100644 src/libxrpl/tx/invariants/VaultInvariant.cpp diff --git a/include/xrpl/tx/InvariantCheck.h b/include/xrpl/tx/InvariantCheck.h deleted file mode 100644 index dc42f9d38c..0000000000 --- a/include/xrpl/tx/InvariantCheck.h +++ /dev/null @@ -1,732 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -namespace xrpl { - -class ReadView; - -#if GENERATING_DOCS -/** - * @brief Prototype for invariant check implementations. - * - * __THIS CLASS DOES NOT EXIST__ - or rather it exists in documentation only to - * communicate the interface required of any invariant checker. Any invariant - * check implementation should implement the public methods documented here. - * - */ -class InvariantChecker_PROTOTYPE -{ -public: - explicit InvariantChecker_PROTOTYPE() = default; - - /** - * @brief called for each ledger entry in the current transaction. - * - * @param isDelete true if the SLE is being deleted - * @param before ledger entry before modification by the transaction - * @param after ledger entry after modification by the transaction - */ - void - visitEntry( - bool isDelete, - std::shared_ptr const& before, - std::shared_ptr const& after); - - /** - * @brief called after all ledger entries have been visited to determine - * the final status of the check - * - * @param tx the transaction being applied - * @param tec the current TER result of the transaction - * @param fee the fee actually charged for this transaction - * @param view a ReadView of the ledger being modified - * @param j journal for logging - * - * @return true if check passes, false if it fails - */ - bool - finalize( - STTx const& tx, - TER const tec, - XRPAmount const fee, - ReadView const& view, - beast::Journal const& j); -}; -#endif - -/** - * @brief Invariant: We should never charge a transaction a negative fee or a - * fee that is larger than what the transaction itself specifies. - * - * We can, in some circumstances, charge less. - */ -class TransactionFeeCheck -{ -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&); -}; - -/** - * @brief Invariant: A transaction must not create XRP and should only destroy - * the XRP fee. - * - * We iterate through all account roots, payment channels and escrow entries - * that were modified and calculate the net change in XRP caused by the - * transactions. - */ -class XRPNotCreated -{ - std::int64_t drops_ = 0; - -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&); -}; - -/** - * @brief Invariant: we cannot remove an account ledger entry - * - * We iterate all account roots that were modified, and ensure that any that - * were present before the transaction was applied continue to be present - * afterwards unless they were explicitly deleted by a successful - * AccountDelete transaction. - */ -class AccountRootsNotDeleted -{ - std::uint32_t accountsDeleted_ = 0; - -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&); -}; - -/** - * @brief Invariant: a deleted account must not have any objects left - * - * We iterate all deleted account roots, and ensure that there are no - * objects left that are directly accessible with that account's ID. - * - * There should only be one deleted account, but that's checked by - * AccountRootsNotDeleted. This invariant will handle multiple deleted account - * roots without a problem. - */ -class AccountRootsDeletedClean -{ - // Pair is . Before is used for most of the checks, so that - // if, for example, an object ID field is cleared, but the object is not - // deleted, it can still be found. After is used specifically for any checks - // that are expected as part of the deletion, such as zeroing out the - // balance. - std::vector, std::shared_ptr>> accountsDeleted_; - -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&); -}; - -/** - * @brief Invariant: An account XRP balance must be in XRP and take a value - * between 0 and INITIAL_XRP drops, inclusive. - * - * We iterate all account roots modified by the transaction and ensure that - * their XRP balances are reasonable. - */ -class XRPBalanceChecks -{ - bool bad_ = false; - -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&); -}; - -/** - * @brief Invariant: corresponding modified ledger entries should match in type - * and added entries should be a valid type. - */ -class LedgerEntryTypesMatch -{ - bool typeMismatch_ = false; - bool invalidTypeAdded_ = false; - -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&); -}; - -/** - * @brief Invariant: Trust lines using XRP are not allowed. - * - * We iterate all the trust lines created by this transaction and ensure - * that they are against a valid issuer. - */ -class NoXRPTrustLines -{ - bool xrpTrustLine_ = false; - -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&); -}; - -/** - * @brief Invariant: Trust lines with deep freeze flag are not allowed if normal - * freeze flag is not set. - * - * We iterate all the trust lines created by this transaction and ensure - * that they don't have deep freeze flag set without normal freeze flag set. - */ -class NoDeepFreezeTrustLinesWithoutFreeze -{ - bool deepFreezeWithoutFreeze_ = false; - -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&); -}; - -/** - * @brief Invariant: frozen trust line balance change is not allowed. - * - * We iterate all affected trust lines and ensure that they don't have - * unexpected change of balance if they're frozen. - */ -class TransfersNotFrozen -{ - struct BalanceChange - { - std::shared_ptr const line; - int const balanceChangeSign; - }; - - struct IssuerChanges - { - std::vector senders; - std::vector receivers; - }; - - using ByIssuer = std::map; - ByIssuer balanceChanges_; - - std::map const> possibleIssuers_; - -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&); - -private: - bool - isValidEntry(std::shared_ptr const& before, std::shared_ptr const& after); - - STAmount - calculateBalanceChange( - std::shared_ptr const& before, - std::shared_ptr const& after, - bool isDelete); - - void - recordBalance(Issue const& issue, BalanceChange change); - - void - recordBalanceChanges(std::shared_ptr const& after, STAmount const& balanceChange); - - std::shared_ptr - findIssuer(AccountID const& issuerID, ReadView const& view); - - bool - validateIssuerChanges( - std::shared_ptr const& issuer, - IssuerChanges const& changes, - STTx const& tx, - beast::Journal const& j, - bool enforce); - - bool - validateFrozenState( - BalanceChange const& change, - bool high, - STTx const& tx, - beast::Journal const& j, - bool enforce, - bool globalFreeze); -}; - -/** - * @brief Invariant: offers should be for non-negative amounts and must not - * be XRP to XRP. - * - * Examine all offers modified by the transaction and ensure that there are - * no offers which contain negative amounts or which exchange XRP for XRP. - */ -class NoBadOffers -{ - bool bad_ = false; - -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&); -}; - -/** - * @brief Invariant: an escrow entry must take a value between 0 and - * INITIAL_XRP drops exclusive. - */ -class NoZeroEscrow -{ - bool bad_ = false; - -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&); -}; - -/** - * @brief Invariant: a new account root must be the consequence of a payment, - * must have the right starting sequence, and the payment - * may not create more than one new account root. - */ -class ValidNewAccountRoot -{ - std::uint32_t accountsCreated_ = 0; - std::uint32_t accountSeq_ = 0; - bool pseudoAccount_ = false; - std::uint32_t flags_ = 0; - -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&); -}; - -/** - * @brief Invariant: Validates several invariants for NFToken pages. - * - * The following checks are made: - * - The page is correctly associated with the owner. - * - The page is correctly ordered between the next and previous links. - * - The page contains at least one and no more than 32 NFTokens. - * - The NFTokens on this page do not belong on a lower or higher page. - * - The NFTokens are correctly sorted on the page. - * - Each URI, if present, is not empty. - */ -class ValidNFTokenPage -{ - bool badEntry_ = false; - bool badLink_ = false; - bool badSort_ = false; - bool badURI_ = false; - bool invalidSize_ = false; - bool deletedFinalPage_ = false; - bool deletedLink_ = false; - -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&); -}; - -/** - * @brief Invariant: Validates counts of NFTokens after all transaction types. - * - * The following checks are made: - * - The number of minted or burned NFTokens can only be changed by - * NFTokenMint or NFTokenBurn transactions. - * - A successful NFTokenMint must increase the number of NFTokens. - * - A failed NFTokenMint must not change the number of minted NFTokens. - * - An NFTokenMint transaction cannot change the number of burned NFTokens. - * - A successful NFTokenBurn must increase the number of burned NFTokens. - * - A failed NFTokenBurn must not change the number of burned NFTokens. - * - An NFTokenBurn transaction cannot change the number of minted NFTokens. - */ -class NFTokenCountTracking -{ - std::uint32_t beforeMintedTotal = 0; - std::uint32_t beforeBurnedTotal = 0; - std::uint32_t afterMintedTotal = 0; - std::uint32_t afterBurnedTotal = 0; - -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&); -}; - -/** - * @brief Invariant: Token holder's trustline balance cannot be negative after - * Clawback. - * - * We iterate all the trust lines affected by this transaction and ensure - * that no more than one trustline is modified, and also holder's balance is - * non-negative. - */ -class ValidClawback -{ - std::uint32_t trustlinesChanged = 0; - std::uint32_t mptokensChanged = 0; - -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 ValidMPTIssuance -{ - std::uint32_t mptIssuancesCreated_ = 0; - std::uint32_t mptIssuancesDeleted_ = 0; - - std::uint32_t mptokensCreated_ = 0; - std::uint32_t mptokensDeleted_ = 0; - // non-MPT transactions may attempt to create - // MPToken by an issuer - bool mptCreatedByIssuer_ = false; - -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&); -}; - -/** - * @brief Invariants: Permissioned Domains must have some rules and - * AcceptedCredentials must have length between 1 and 10 inclusive. - * - * Since only permissions constitute rules, an empty credentials list - * means that there are no rules and the invariant is violated. - * - * Credentials must be sorted and no duplicates allowed - * - */ -class ValidPermissionedDomain -{ - struct SleStatus - { - std::size_t credentialsSize_{0}; - bool isSorted_ = false; - bool isUnique_ = false; - bool isDelete_ = false; - }; - std::vector sleStatus_; - -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&); -}; - -/** - * @brief Invariants: Pseudo-accounts have valid and consistent 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; - bool badHybrids_ = false; - hash_set domains_; - -public: - void - visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); - - bool - finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); -}; - -class ValidAMM -{ - std::optional ammAccount_; - std::optional lptAMMBalanceAfter_; - std::optional lptAMMBalanceBefore_; - bool ammPoolChanged_; - -public: - enum class ZeroAllowed : bool { No = false, Yes = true }; - - ValidAMM() : ammPoolChanged_{false} - { - } - void - visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); - - bool - finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); - -private: - bool - finalizeBid(bool enforce, beast::Journal const&) const; - bool - finalizeVote(bool enforce, beast::Journal const&) const; - bool - finalizeCreate(STTx const&, ReadView const&, bool enforce, beast::Journal const&) const; - bool - finalizeDelete(bool enforce, TER res, beast::Journal const&) const; - bool - finalizeDeposit(STTx const&, ReadView const&, bool enforce, beast::Journal const&) const; - // Includes clawback - bool - finalizeWithdraw(STTx const&, ReadView const&, bool enforce, beast::Journal const&) const; - bool - finalizeDEX(bool enforce, beast::Journal const&) const; - bool - generalInvariant(STTx const&, ReadView const&, ZeroAllowed zeroAllowed, beast::Journal const&) - const; -}; - -/** - * @brief Invariants: Some fields are unmodifiable - * - * Check that any fields specified as unmodifiable are not modified when the - * object is modified. Creation and deletion are ignored. - * - */ -class NoModifiedUnmodifiableFields -{ - // Pair is . - std::set> changedEntries_; - -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&); -}; - -/** - * @brief Invariants: Loan brokers are internally consistent - * - * 1. If `LoanBroker.OwnerCount = 0` the `DirectoryNode` will have at most one - * node (the root), which will only hold entries for `RippleState` or - * `MPToken` objects. - * - */ -class ValidLoanBroker -{ - // Not all of these elements will necessarily be populated. Remaining items - // will be looked up as needed. - struct BrokerInfo - { - SLE::const_pointer brokerBefore = nullptr; - // After is used for most of the checks, except - // those that check changed values. - SLE::const_pointer brokerAfter = nullptr; - }; - // Collect all the LoanBrokers found directly or indirectly through - // pseudo-accounts. Key is the brokerID / index. It will be used to find the - // LoanBroker object if brokerBefore and brokerAfter are nullptr - std::map brokers_; - // Collect all the modified trust lines. Their high and low accounts will be - // loaded to look for LoanBroker pseudo-accounts. - std::vector lines_; - // Collect all the modified MPTokens. Their accounts will be loaded to look - // for LoanBroker pseudo-accounts. - std::vector mpts_; - - bool - goodZeroDirectory(ReadView const& view, SLE::const_ref dir, beast::Journal const& j) const; - -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&); -}; - -/** - * @brief Invariants: Loans are internally consistent - * - * 1. If `Loan.PaymentRemaining = 0` then `Loan.PrincipalOutstanding = 0` - * - */ -class ValidLoan -{ - // Pair is . After is used for most of the checks, except - // those that check changed values. - std::vector> loans_; - -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&); -}; - -/* - * @brief Invariants: Vault object and MPTokenIssuance for vault shares - * - * - vault deleted and vault created is empty - * - vault created must be linked to pseudo-account for shares and assets - * - vault must have MPTokenIssuance for shares - * - vault without shares outstanding must have no shares - * - loss unrealized does not exceed the difference between assets total and - * assets available - * - assets available do not exceed assets total - * - vault deposit increases assets and share issuance, and adds to: - * total assets, assets available, shares outstanding - * - vault withdrawal and clawback reduce assets and share issuance, and - * subtracts from: total assets, assets available, shares outstanding - * - vault set must not alter the vault assets or shares balance - * - no vault transaction can change loss unrealized (it's updated by loan - * transactions) - * - */ -class ValidVault -{ - Number static constexpr zero{}; - - struct Vault final - { - uint256 key = beast::zero; - Asset asset = {}; - AccountID pseudoId = {}; - AccountID owner = {}; - uint192 shareMPTID = beast::zero; - Number assetsTotal = 0; - Number assetsAvailable = 0; - Number assetsMaximum = 0; - Number lossUnrealized = 0; - - Vault static make(SLE const&); - }; - - struct Shares final - { - MPTIssue share = {}; - std::uint64_t sharesTotal = 0; - std::uint64_t sharesMaximum = 0; - - Shares static make(SLE const&); - }; - - std::vector afterVault_ = {}; - std::vector afterMPTs_ = {}; - std::vector beforeVault_ = {}; - std::vector beforeMPTs_ = {}; - std::unordered_map deltas_ = {}; - -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&); -}; - -// additional invariant checks can be declared above and then added to this -// tuple -using InvariantChecks = std::tuple< - TransactionFeeCheck, - AccountRootsNotDeleted, - AccountRootsDeletedClean, - LedgerEntryTypesMatch, - XRPBalanceChecks, - XRPNotCreated, - NoXRPTrustLines, - NoDeepFreezeTrustLinesWithoutFreeze, - TransfersNotFrozen, - NoBadOffers, - NoZeroEscrow, - ValidNewAccountRoot, - ValidNFTokenPage, - NFTokenCountTracking, - ValidClawback, - ValidMPTIssuance, - ValidPermissionedDomain, - ValidPermissionedDEX, - ValidAMM, - NoModifiedUnmodifiableFields, - ValidPseudoAccounts, - ValidLoanBroker, - ValidLoan, - ValidVault>; - -/** - * @brief get a tuple of all invariant checks - * - * @return std::tuple of instances that implement the required invariant check - * methods - * - * @see xrpl::InvariantChecker_PROTOTYPE - */ -inline InvariantChecks -getInvariantChecks() -{ - return InvariantChecks{}; -} - -} // namespace xrpl diff --git a/include/xrpl/tx/invariants/AMMInvariant.h b/include/xrpl/tx/invariants/AMMInvariant.h new file mode 100644 index 0000000000..63ebb804ae --- /dev/null +++ b/include/xrpl/tx/invariants/AMMInvariant.h @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +class ValidAMM +{ + std::optional ammAccount_; + std::optional lptAMMBalanceAfter_; + std::optional lptAMMBalanceBefore_; + bool ammPoolChanged_; + +public: + enum class ZeroAllowed : bool { No = false, Yes = true }; + + ValidAMM() : ammPoolChanged_{false} + { + } + void + visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); + + bool + finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); + +private: + bool + finalizeBid(bool enforce, beast::Journal const&) const; + bool + finalizeVote(bool enforce, beast::Journal const&) const; + bool + finalizeCreate(STTx const&, ReadView const&, bool enforce, beast::Journal const&) const; + bool + finalizeDelete(bool enforce, TER res, beast::Journal const&) const; + bool + finalizeDeposit(STTx const&, ReadView const&, bool enforce, beast::Journal const&) const; + // Includes clawback + bool + finalizeWithdraw(STTx const&, ReadView const&, bool enforce, beast::Journal const&) const; + bool + finalizeDEX(bool enforce, beast::Journal const&) const; + bool + generalInvariant(STTx const&, ReadView const&, ZeroAllowed zeroAllowed, beast::Journal const&) + const; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/invariants/FreezeInvariant.h b/include/xrpl/tx/invariants/FreezeInvariant.h new file mode 100644 index 0000000000..ac9d83166e --- /dev/null +++ b/include/xrpl/tx/invariants/FreezeInvariant.h @@ -0,0 +1,84 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl { + +/** + * @brief Invariant: frozen trust line balance change is not allowed. + * + * We iterate all affected trust lines and ensure that they don't have + * unexpected change of balance if they're frozen. + */ +class TransfersNotFrozen +{ + struct BalanceChange + { + std::shared_ptr const line; + int const balanceChangeSign; + }; + + struct IssuerChanges + { + std::vector senders; + std::vector receivers; + }; + + using ByIssuer = std::map; + ByIssuer balanceChanges_; + + std::map const> possibleIssuers_; + +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&); + +private: + bool + isValidEntry(std::shared_ptr const& before, std::shared_ptr const& after); + + STAmount + calculateBalanceChange( + std::shared_ptr const& before, + std::shared_ptr const& after, + bool isDelete); + + void + recordBalance(Issue const& issue, BalanceChange change); + + void + recordBalanceChanges(std::shared_ptr const& after, STAmount const& balanceChange); + + std::shared_ptr + findIssuer(AccountID const& issuerID, ReadView const& view); + + bool + validateIssuerChanges( + std::shared_ptr const& issuer, + IssuerChanges const& changes, + STTx const& tx, + beast::Journal const& j, + bool enforce); + + bool + validateFrozenState( + BalanceChange const& change, + bool high, + STTx const& tx, + beast::Journal const& j, + bool enforce, + bool globalFreeze); +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/invariants/InvariantCheck.h b/include/xrpl/tx/invariants/InvariantCheck.h new file mode 100644 index 0000000000..5ded5980da --- /dev/null +++ b/include/xrpl/tx/invariants/InvariantCheck.h @@ -0,0 +1,385 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl { + +#if GENERATING_DOCS +/** + * @brief Prototype for invariant check implementations. + * + * __THIS CLASS DOES NOT EXIST__ - or rather it exists in documentation only to + * communicate the interface required of any invariant checker. Any invariant + * check implementation should implement the public methods documented here. + * + */ +class InvariantChecker_PROTOTYPE +{ +public: + explicit InvariantChecker_PROTOTYPE() = default; + + /** + * @brief called for each ledger entry in the current transaction. + * + * @param isDelete true if the SLE is being deleted + * @param before ledger entry before modification by the transaction + * @param after ledger entry after modification by the transaction + */ + void + visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after); + + /** + * @brief called after all ledger entries have been visited to determine + * the final status of the check + * + * @param tx the transaction being applied + * @param tec the current TER result of the transaction + * @param fee the fee actually charged for this transaction + * @param view a ReadView of the ledger being modified + * @param j journal for logging + * + * @return true if check passes, false if it fails + */ + bool + finalize( + STTx const& tx, + TER const tec, + XRPAmount const fee, + ReadView const& view, + beast::Journal const& j); +}; +#endif + +/** + * @brief Invariant: We should never charge a transaction a negative fee or a + * fee that is larger than what the transaction itself specifies. + * + * We can, in some circumstances, charge less. + */ +class TransactionFeeCheck +{ +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&); +}; + +/** + * @brief Invariant: A transaction must not create XRP and should only destroy + * the XRP fee. + * + * We iterate through all account roots, payment channels and escrow entries + * that were modified and calculate the net change in XRP caused by the + * transactions. + */ +class XRPNotCreated +{ + std::int64_t drops_ = 0; + +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&); +}; + +/** + * @brief Invariant: we cannot remove an account ledger entry + * + * We iterate all account roots that were modified, and ensure that any that + * were present before the transaction was applied continue to be present + * afterwards unless they were explicitly deleted by a successful + * AccountDelete transaction. + */ +class AccountRootsNotDeleted +{ + std::uint32_t accountsDeleted_ = 0; + +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&); +}; + +/** + * @brief Invariant: a deleted account must not have any objects left + * + * We iterate all deleted account roots, and ensure that there are no + * objects left that are directly accessible with that account's ID. + * + * There should only be one deleted account, but that's checked by + * AccountRootsNotDeleted. This invariant will handle multiple deleted account + * roots without a problem. + */ +class AccountRootsDeletedClean +{ + // Pair is . Before is used for most of the checks, so that + // if, for example, an object ID field is cleared, but the object is not + // deleted, it can still be found. After is used specifically for any checks + // that are expected as part of the deletion, such as zeroing out the + // balance. + std::vector, std::shared_ptr>> accountsDeleted_; + +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&); +}; + +/** + * @brief Invariant: An account XRP balance must be in XRP and take a value + * between 0 and INITIAL_XRP drops, inclusive. + * + * We iterate all account roots modified by the transaction and ensure that + * their XRP balances are reasonable. + */ +class XRPBalanceChecks +{ + bool bad_ = false; + +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&); +}; + +/** + * @brief Invariant: corresponding modified ledger entries should match in type + * and added entries should be a valid type. + */ +class LedgerEntryTypesMatch +{ + bool typeMismatch_ = false; + bool invalidTypeAdded_ = false; + +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&); +}; + +/** + * @brief Invariant: Trust lines using XRP are not allowed. + * + * We iterate all the trust lines created by this transaction and ensure + * that they are against a valid issuer. + */ +class NoXRPTrustLines +{ + bool xrpTrustLine_ = false; + +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&); +}; + +/** + * @brief Invariant: Trust lines with deep freeze flag are not allowed if normal + * freeze flag is not set. + * + * We iterate all the trust lines created by this transaction and ensure + * that they don't have deep freeze flag set without normal freeze flag set. + */ +class NoDeepFreezeTrustLinesWithoutFreeze +{ + bool deepFreezeWithoutFreeze_ = false; + +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&); +}; + +/** + * @brief Invariant: offers should be for non-negative amounts and must not + * be XRP to XRP. + * + * Examine all offers modified by the transaction and ensure that there are + * no offers which contain negative amounts or which exchange XRP for XRP. + */ +class NoBadOffers +{ + bool bad_ = false; + +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&); +}; + +/** + * @brief Invariant: an escrow entry must take a value between 0 and + * INITIAL_XRP drops exclusive. + */ +class NoZeroEscrow +{ + bool bad_ = false; + +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&); +}; + +/** + * @brief Invariant: a new account root must be the consequence of a payment, + * must have the right starting sequence, and the payment + * may not create more than one new account root. + */ +class ValidNewAccountRoot +{ + std::uint32_t accountsCreated_ = 0; + std::uint32_t accountSeq_ = 0; + bool pseudoAccount_ = false; + std::uint32_t flags_ = 0; + +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&); +}; + +/** + * @brief Invariant: Token holder's trustline balance cannot be negative after + * Clawback. + * + * We iterate all the trust lines affected by this transaction and ensure + * that no more than one trustline is modified, and also holder's balance is + * non-negative. + */ +class ValidClawback +{ + std::uint32_t trustlinesChanged = 0; + std::uint32_t mptokensChanged = 0; + +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&); +}; + +/** + * @brief Invariants: Pseudo-accounts have valid and consistent 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&); +}; + +/** + * @brief Invariants: Some fields are unmodifiable + * + * Check that any fields specified as unmodifiable are not modified when the + * object is modified. Creation and deletion are ignored. + * + */ +class NoModifiedUnmodifiableFields +{ + // Pair is . + std::set> changedEntries_; + +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&); +}; + +// additional invariant checks can be declared above and then added to this +// tuple +using InvariantChecks = std::tuple< + TransactionFeeCheck, + AccountRootsNotDeleted, + AccountRootsDeletedClean, + LedgerEntryTypesMatch, + XRPBalanceChecks, + XRPNotCreated, + NoXRPTrustLines, + NoDeepFreezeTrustLinesWithoutFreeze, + TransfersNotFrozen, + NoBadOffers, + NoZeroEscrow, + ValidNewAccountRoot, + ValidNFTokenPage, + NFTokenCountTracking, + ValidClawback, + ValidMPTIssuance, + ValidPermissionedDomain, + ValidPermissionedDEX, + ValidAMM, + NoModifiedUnmodifiableFields, + ValidPseudoAccounts, + ValidLoanBroker, + ValidLoan, + ValidVault>; + +/** + * @brief get a tuple of all invariant checks + * + * @return std::tuple of instances that implement the required invariant check + * methods + * + * @see xrpl::InvariantChecker_PROTOTYPE + */ +inline InvariantChecks +getInvariantChecks() +{ + return InvariantChecks{}; +} + +} // namespace xrpl diff --git a/include/xrpl/tx/invariants/InvariantCheckPrivilege.h b/include/xrpl/tx/invariants/InvariantCheckPrivilege.h new file mode 100644 index 0000000000..161b3572db --- /dev/null +++ b/include/xrpl/tx/invariants/InvariantCheckPrivilege.h @@ -0,0 +1,60 @@ +#pragma once + +#include + +#include + +namespace xrpl { + +/* +assert(enforce) + +There are several asserts (or XRPL_ASSERTs) in invariant check files 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. + mustModifyVault = 0x0800, // The transaction must modify, delete or create, a vault + mayModifyVault = 0x1000, // The transaction MAY modify, delete or create, a vault +}; + +constexpr Privilege +operator|(Privilege lhs, Privilege rhs) +{ + return safe_cast( + safe_cast>(lhs) | + safe_cast>(rhs)); +} + +bool +hasPrivilege(STTx const& tx, Privilege priv); + +} // namespace xrpl diff --git a/include/xrpl/tx/invariants/LoanInvariant.h b/include/xrpl/tx/invariants/LoanInvariant.h new file mode 100644 index 0000000000..be771cd582 --- /dev/null +++ b/include/xrpl/tx/invariants/LoanInvariant.h @@ -0,0 +1,75 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl { + +/** + * @brief Invariants: Loan brokers are internally consistent + * + * 1. If `LoanBroker.OwnerCount = 0` the `DirectoryNode` will have at most one + * node (the root), which will only hold entries for `RippleState` or + * `MPToken` objects. + * + */ +class ValidLoanBroker +{ + // Not all of these elements will necessarily be populated. Remaining items + // will be looked up as needed. + struct BrokerInfo + { + SLE::const_pointer brokerBefore = nullptr; + // After is used for most of the checks, except + // those that check changed values. + SLE::const_pointer brokerAfter = nullptr; + }; + // Collect all the LoanBrokers found directly or indirectly through + // pseudo-accounts. Key is the brokerID / index. It will be used to find the + // LoanBroker object if brokerBefore and brokerAfter are nullptr + std::map brokers_; + // Collect all the modified trust lines. Their high and low accounts will be + // loaded to look for LoanBroker pseudo-accounts. + std::vector lines_; + // Collect all the modified MPTokens. Their accounts will be loaded to look + // for LoanBroker pseudo-accounts. + std::vector mpts_; + + bool + goodZeroDirectory(ReadView const& view, SLE::const_ref dir, beast::Journal const& j) const; + +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&); +}; + +/** + * @brief Invariants: Loans are internally consistent + * + * 1. If `Loan.PaymentRemaining = 0` then `Loan.PrincipalOutstanding = 0` + * + */ +class ValidLoan +{ + // Pair is . After is used for most of the checks, except + // those that check changed values. + std::vector> loans_; + +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&); +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/invariants/MPTInvariant.h b/include/xrpl/tx/invariants/MPTInvariant.h new file mode 100644 index 0000000000..b6533c263d --- /dev/null +++ b/include/xrpl/tx/invariants/MPTInvariant.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include +#include +#include + +#include + +namespace xrpl { + +class ValidMPTIssuance +{ + std::uint32_t mptIssuancesCreated_ = 0; + std::uint32_t mptIssuancesDeleted_ = 0; + + std::uint32_t mptokensCreated_ = 0; + std::uint32_t mptokensDeleted_ = 0; + // non-MPT transactions may attempt to create + // MPToken by an issuer + bool mptCreatedByIssuer_ = false; + +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&); +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/invariants/NFTInvariant.h b/include/xrpl/tx/invariants/NFTInvariant.h new file mode 100644 index 0000000000..8a88ca1c63 --- /dev/null +++ b/include/xrpl/tx/invariants/NFTInvariant.h @@ -0,0 +1,70 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +/** + * @brief Invariant: Validates several invariants for NFToken pages. + * + * The following checks are made: + * - The page is correctly associated with the owner. + * - The page is correctly ordered between the next and previous links. + * - The page contains at least one and no more than 32 NFTokens. + * - The NFTokens on this page do not belong on a lower or higher page. + * - The NFTokens are correctly sorted on the page. + * - Each URI, if present, is not empty. + */ +class ValidNFTokenPage +{ + bool badEntry_ = false; + bool badLink_ = false; + bool badSort_ = false; + bool badURI_ = false; + bool invalidSize_ = false; + bool deletedFinalPage_ = false; + bool deletedLink_ = false; + +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&); +}; + +/** + * @brief Invariant: Validates counts of NFTokens after all transaction types. + * + * The following checks are made: + * - The number of minted or burned NFTokens can only be changed by + * NFTokenMint or NFTokenBurn transactions. + * - A successful NFTokenMint must increase the number of NFTokens. + * - A failed NFTokenMint must not change the number of minted NFTokens. + * - An NFTokenMint transaction cannot change the number of burned NFTokens. + * - A successful NFTokenBurn must increase the number of burned NFTokens. + * - A failed NFTokenBurn must not change the number of burned NFTokens. + * - An NFTokenBurn transaction cannot change the number of minted NFTokens. + */ +class NFTokenCountTracking +{ + std::uint32_t beforeMintedTotal = 0; + std::uint32_t beforeBurnedTotal = 0; + std::uint32_t afterMintedTotal = 0; + std::uint32_t afterBurnedTotal = 0; + +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&); +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/invariants/PermissionedDEXInvariant.h b/include/xrpl/tx/invariants/PermissionedDEXInvariant.h new file mode 100644 index 0000000000..b4e06cd212 --- /dev/null +++ b/include/xrpl/tx/invariants/PermissionedDEXInvariant.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace xrpl { + +class ValidPermissionedDEX +{ + bool regularOffers_ = false; + bool badHybrids_ = false; + hash_set domains_; + +public: + void + visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); + + bool + finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/invariants/PermissionedDomainInvariant.h b/include/xrpl/tx/invariants/PermissionedDomainInvariant.h new file mode 100644 index 0000000000..f6c902ecb2 --- /dev/null +++ b/include/xrpl/tx/invariants/PermissionedDomainInvariant.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include +#include + +#include + +namespace xrpl { + +/** + * @brief Invariants: Permissioned Domains must have some rules and + * AcceptedCredentials must have length between 1 and 10 inclusive. + * + * Since only permissions constitute rules, an empty credentials list + * means that there are no rules and the invariant is violated. + * + * Credentials must be sorted and no duplicates allowed + * + */ +class ValidPermissionedDomain +{ + struct SleStatus + { + std::size_t credentialsSize_{0}; + bool isSorted_ = false; + bool isUnique_ = false; + bool isDelete_ = false; + }; + std::vector sleStatus_; + +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&); +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/invariants/VaultInvariant.h b/include/xrpl/tx/invariants/VaultInvariant.h new file mode 100644 index 0000000000..ded9e4618b --- /dev/null +++ b/include/xrpl/tx/invariants/VaultInvariant.h @@ -0,0 +1,77 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl { + +/* + * @brief Invariants: Vault object and MPTokenIssuance for vault shares + * + * - vault deleted and vault created is empty + * - vault created must be linked to pseudo-account for shares and assets + * - vault must have MPTokenIssuance for shares + * - vault without shares outstanding must have no shares + * - loss unrealized does not exceed the difference between assets total and + * assets available + * - assets available do not exceed assets total + * - vault deposit increases assets and share issuance, and adds to: + * total assets, assets available, shares outstanding + * - vault withdrawal and clawback reduce assets and share issuance, and + * subtracts from: total assets, assets available, shares outstanding + * - vault set must not alter the vault assets or shares balance + * - no vault transaction can change loss unrealized (it's updated by loan + * transactions) + * + */ +class ValidVault +{ + Number static constexpr zero{}; + + struct Vault final + { + uint256 key = beast::zero; + Asset asset = {}; + AccountID pseudoId = {}; + AccountID owner = {}; + uint192 shareMPTID = beast::zero; + Number assetsTotal = 0; + Number assetsAvailable = 0; + Number assetsMaximum = 0; + Number lossUnrealized = 0; + + Vault static make(SLE const&); + }; + + struct Shares final + { + MPTIssue share = {}; + std::uint64_t sharesTotal = 0; + std::uint64_t sharesMaximum = 0; + + Shares static make(SLE const&); + }; + + std::vector afterVault_ = {}; + std::vector afterMPTs_ = {}; + std::vector beforeVault_ = {}; + std::vector beforeMPTs_ = {}; + std::unordered_map deltas_ = {}; + +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&); +}; + +} // namespace xrpl diff --git a/src/libxrpl/tx/ApplyContext.cpp b/src/libxrpl/tx/ApplyContext.cpp index a8eca09ff2..f62c63d1e6 100644 --- a/src/libxrpl/tx/ApplyContext.cpp +++ b/src/libxrpl/tx/ApplyContext.cpp @@ -1,8 +1,9 @@ +#include +// #include #include #include -#include -#include +#include namespace xrpl { diff --git a/src/libxrpl/tx/InvariantCheck.cpp b/src/libxrpl/tx/InvariantCheck.cpp deleted file mode 100644 index b94f11100b..0000000000 --- a/src/libxrpl/tx/InvariantCheck.cpp +++ /dev/null @@ -1,3483 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -namespace xrpl { - -/* -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. - mustModifyVault = 0x0800, // The transaction must modify, delete or create, a vault - mayModifyVault = 0x1000, // The transaction MAY modify, delete or create, a vault -}; -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, delegable, 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, - std::shared_ptr const&, - std::shared_ptr const&) -{ - // nothing to do -} - -bool -TransactionFeeCheck::finalize( - STTx const& tx, - TER const, - XRPAmount const fee, - ReadView const&, - beast::Journal const& j) -{ - // We should never charge a negative fee - if (fee.drops() < 0) - { - JLOG(j.fatal()) << "Invariant failed: fee paid was negative: " << fee.drops(); - return false; - } - - // We should never charge a fee that's greater than or equal to the - // entire XRP supply. - if (fee >= INITIAL_XRP) - { - JLOG(j.fatal()) << "Invariant failed: fee paid exceeds system limit: " << fee.drops(); - return false; - } - - // We should never charge more for a transaction than the transaction - // authorizes. It's possible to charge less in some circumstances. - if (fee > tx.getFieldAmount(sfFee).xrp()) - { - JLOG(j.fatal()) << "Invariant failed: fee paid is " << fee.drops() - << " exceeds fee specified in transaction."; - return false; - } - - return true; -} - -//------------------------------------------------------------------------------ - -void -XRPNotCreated::visitEntry( - bool isDelete, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - /* We go through all modified ledger entries, looking only at account roots, - * escrow payments, and payment channels. We remove from the total any - * previous XRP values and add to the total any new XRP values. The net - * balance of a payment channel is computed from two fields (amount and - * balance) and deletions are ignored for paychan and escrow because the - * amount fields have not been adjusted for those in the case of deletion. - */ - if (before) - { - switch (before->getType()) - { - case ltACCOUNT_ROOT: - drops_ -= (*before)[sfBalance].xrp().drops(); - break; - case ltPAYCHAN: - drops_ -= ((*before)[sfAmount] - (*before)[sfBalance]).xrp().drops(); - break; - case ltESCROW: - if (isXRP((*before)[sfAmount])) - drops_ -= (*before)[sfAmount].xrp().drops(); - break; - default: - break; - } - } - - if (after) - { - switch (after->getType()) - { - case ltACCOUNT_ROOT: - drops_ += (*after)[sfBalance].xrp().drops(); - break; - case ltPAYCHAN: - if (!isDelete) - drops_ += ((*after)[sfAmount] - (*after)[sfBalance]).xrp().drops(); - break; - case ltESCROW: - if (!isDelete && isXRP((*after)[sfAmount])) - drops_ += (*after)[sfAmount].xrp().drops(); - break; - default: - break; - } - } -} - -bool -XRPNotCreated::finalize( - STTx const& tx, - TER const, - XRPAmount const fee, - ReadView const&, - beast::Journal const& j) -{ - // The net change should never be positive, as this would mean that the - // transaction created XRP out of thin air. That's not possible. - if (drops_ > 0) - { - JLOG(j.fatal()) << "Invariant failed: XRP net change was positive: " << drops_; - return false; - } - - // The negative of the net change should be equal to actual fee charged. - if (-drops_ != fee.drops()) - { - JLOG(j.fatal()) << "Invariant failed: XRP net change of " << drops_ << " doesn't match fee " - << fee.drops(); - return false; - } - - return true; -} - -//------------------------------------------------------------------------------ - -void -XRPBalanceChecks::visitEntry( - bool, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - auto isBad = [](STAmount const& balance) { - if (!balance.native()) - return true; - - auto const drops = balance.xrp(); - - // Can't have more than the number of drops instantiated - // in the genesis ledger. - if (drops > INITIAL_XRP) - return true; - - // Can't have a negative balance (0 is OK) - if (drops < XRPAmount{0}) - return true; - - return false; - }; - - if (before && before->getType() == ltACCOUNT_ROOT) - bad_ |= isBad((*before)[sfBalance]); - - if (after && after->getType() == ltACCOUNT_ROOT) - bad_ |= isBad((*after)[sfBalance]); -} - -bool -XRPBalanceChecks::finalize( - STTx const&, - TER const, - XRPAmount const, - ReadView const&, - beast::Journal const& j) -{ - if (bad_) - { - JLOG(j.fatal()) << "Invariant failed: incorrect account XRP balance"; - return false; - } - - return true; -} - -//------------------------------------------------------------------------------ - -void -NoBadOffers::visitEntry( - bool isDelete, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - auto isBad = [](STAmount const& pays, STAmount const& gets) { - // An offer should never be negative - if (pays < beast::zero) - return true; - - if (gets < beast::zero) - return true; - - // Can't have an XRP to XRP offer: - return pays.native() && gets.native(); - }; - - if (before && before->getType() == ltOFFER) - bad_ |= isBad((*before)[sfTakerPays], (*before)[sfTakerGets]); - - if (after && after->getType() == ltOFFER) - bad_ |= isBad((*after)[sfTakerPays], (*after)[sfTakerGets]); -} - -bool -NoBadOffers::finalize( - STTx const&, - TER const, - XRPAmount const, - ReadView const&, - beast::Journal const& j) -{ - if (bad_) - { - JLOG(j.fatal()) << "Invariant failed: offer with a bad amount"; - return false; - } - - return true; -} - -//------------------------------------------------------------------------------ - -void -NoZeroEscrow::visitEntry( - bool isDelete, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - auto isBad = [](STAmount const& amount) { - // XRP case - if (amount.native()) - { - if (amount.xrp() <= XRPAmount{0}) - return true; - - if (amount.xrp() >= INITIAL_XRP) - return true; - } - else - { - // IOU case - if (amount.holds()) - { - if (amount <= beast::zero) - return true; - - if (badCurrency() == amount.getCurrency()) - return true; - } - - // MPT case - if (amount.holds()) - { - if (amount <= beast::zero) - return true; - - if (amount.mpt() > MPTAmount{maxMPTokenAmount}) - return true; // LCOV_EXCL_LINE - } - } - return false; - }; - - if (before && before->getType() == ltESCROW) - bad_ |= isBad((*before)[sfAmount]); - - if (after && after->getType() == ltESCROW) - bad_ |= isBad((*after)[sfAmount]); - - auto checkAmount = [this](std::int64_t amount) { - if (amount > maxMPTokenAmount || amount < 0) - bad_ = true; - }; - - if (after && after->getType() == ltMPTOKEN_ISSUANCE) - { - auto const outstanding = (*after)[sfOutstandingAmount]; - checkAmount(outstanding); - if (auto const locked = (*after)[~sfLockedAmount]) - { - checkAmount(*locked); - bad_ = outstanding < *locked; - } - } - - if (after && after->getType() == ltMPTOKEN) - { - auto const mptAmount = (*after)[sfMPTAmount]; - checkAmount(mptAmount); - if (auto const locked = (*after)[~sfLockedAmount]) - { - checkAmount(*locked); - } - } -} - -bool -NoZeroEscrow::finalize( - STTx const& txn, - TER const, - XRPAmount const, - ReadView const& rv, - beast::Journal const& j) -{ - if (bad_) - { - JLOG(j.fatal()) << "Invariant failed: escrow specifies invalid amount"; - return false; - } - - return true; -} - -//------------------------------------------------------------------------------ - -void -AccountRootsNotDeleted::visitEntry( - bool isDelete, - std::shared_ptr const& before, - std::shared_ptr const&) -{ - if (isDelete && before && before->getType() == ltACCOUNT_ROOT) - accountsDeleted_++; -} - -bool -AccountRootsNotDeleted::finalize( - STTx const& tx, - TER const result, - XRPAmount const, - ReadView const&, - beast::Journal const& j) -{ - // AMM account root can be deleted as the result of AMM withdraw/delete - // transaction when the total AMM LP Tokens balance goes to 0. - // A successful AccountDelete or AMMDelete MUST delete exactly - // one account root. - if (hasPrivilege(tx, mustDeleteAcct) && result == tesSUCCESS) - { - if (accountsDeleted_ == 1) - return true; - - if (accountsDeleted_ == 0) - JLOG(j.fatal()) << "Invariant failed: account deletion " - "succeeded without deleting an account"; - else - JLOG(j.fatal()) << "Invariant failed: account deletion " - "succeeded but deleted multiple accounts!"; - return false; - } - - // 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 (hasPrivilege(tx, mayDeleteAcct) && result == tesSUCCESS && accountsDeleted_ == 1) - return true; - - if (accountsDeleted_ == 0) - return true; - - JLOG(j.fatal()) << "Invariant failed: an account root was deleted"; - return false; -} - -//------------------------------------------------------------------------------ - -void -AccountRootsDeletedClean::visitEntry( - bool isDelete, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - if (isDelete && before && before->getType() == ltACCOUNT_ROOT) - accountsDeleted_.emplace_back(before, after); -} - -bool -AccountRootsDeletedClean::finalize( - STTx const& tx, - TER const result, - XRPAmount const, - ReadView const& view, - beast::Journal const& j) -{ - // Always check for objects in the ledger, but to prevent differing - // transaction processing results, however unlikely, only fail if the - // 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(featureSingleAssetVault) || - view.rules().enabled(featureLendingProtocol); - - auto const objectExists = [&view, enforce, &j](auto const& keylet) { - (void)enforce; - if (auto const sle = view.read(keylet)) - { - // Finding the object is bad - auto const typeName = [&sle]() { - auto item = LedgerFormats::getInstance().findByType(sle->getType()); - - if (item != nullptr) - return item->getName(); - return std::to_string(sle->getType()); - }(); - - 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, - "xrpl::AccountRootsDeletedClean::finalize::objectExists : " - "account deletion left no objects behind"); - return true; - } - return false; - }; - - for (auto const& [before, after] : accountsDeleted_) - { - auto const accountID = before->getAccountID(sfAccount); - // An account should not be deleted with a balance - if (after->at(sfBalance) != beast::zero) - { - JLOG(j.fatal()) << "Invariant failed: account deletion left " - "behind a non-zero balance"; - XRPL_ASSERT( - enforce, - "xrpl::AccountRootsDeletedClean::finalize : " - "deleted account has zero balance"); - if (enforce) - return false; - } - // An account should not be deleted with a non-zero owner count - if (after->at(sfOwnerCount) != 0) - { - JLOG(j.fatal()) << "Invariant failed: account deletion left " - "behind a non-zero owner count"; - XRPL_ASSERT( - enforce, - "xrpl::AccountRootsDeletedClean::finalize : " - "deleted account has zero owner count"); - if (enforce) - return false; - } - // Simple types - for (auto const& [keyletfunc, _, __] : directAccountKeylets) - { - if (objectExists(std::invoke(keyletfunc, accountID)) && enforce) - return false; - } - - { - // NFT pages. nftpage_min and nftpage_max were already explicitly - // checked above as entries in directAccountKeylets. This uses - // view.succ() to check for any NFT pages in between the two - // endpoints. - Keylet const first = keylet::nftpage_min(accountID); - Keylet const last = keylet::nftpage_max(accountID); - - std::optional key = view.succ(first.key, last.key.next()); - - // current page - if (key && objectExists(Keylet{ltNFTOKEN_PAGE, *key}) && enforce) - return false; - } - - // 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 (before->isFieldPresent(*field)) - { - auto const key = before->getFieldH256(*field); - if (objectExists(keylet::unchecked(key)) && enforce) - return false; - } - } - } - - return true; -} - -//------------------------------------------------------------------------------ - -void -LedgerEntryTypesMatch::visitEntry( - bool, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - if (before && after && before->getType() != after->getType()) - typeMismatch_ = true; - - if (after) - { -#pragma push_macro("LEDGER_ENTRY") -#undef LEDGER_ENTRY - -#define LEDGER_ENTRY(tag, ...) case tag: - - switch (after->getType()) - { -#include - - break; - default: - invalidTypeAdded_ = true; - break; - } - -#undef LEDGER_ENTRY -#pragma pop_macro("LEDGER_ENTRY") - } -} - -bool -LedgerEntryTypesMatch::finalize( - STTx const&, - TER const, - XRPAmount const, - ReadView const&, - beast::Journal const& j) -{ - if ((!typeMismatch_) && (!invalidTypeAdded_)) - return true; - - if (typeMismatch_) - { - JLOG(j.fatal()) << "Invariant failed: ledger entry type mismatch"; - } - - if (invalidTypeAdded_) - { - JLOG(j.fatal()) << "Invariant failed: invalid ledger entry type added"; - } - - return false; -} - -//------------------------------------------------------------------------------ - -void -NoXRPTrustLines::visitEntry( - bool, - std::shared_ptr const&, - std::shared_ptr const& after) -{ - if (after && after->getType() == ltRIPPLE_STATE) - { - // checking the issue directly here instead of - // relying on .native() just in case native somehow - // were systematically incorrect - xrpTrustLine_ = after->getFieldAmount(sfLowLimit).issue() == xrpIssue() || - after->getFieldAmount(sfHighLimit).issue() == xrpIssue(); - } -} - -bool -NoXRPTrustLines::finalize( - STTx const&, - TER const, - XRPAmount const, - ReadView const&, - beast::Journal const& j) -{ - if (!xrpTrustLine_) - return true; - - JLOG(j.fatal()) << "Invariant failed: an XRP trust line was created"; - return false; -} - -//------------------------------------------------------------------------------ - -void -NoDeepFreezeTrustLinesWithoutFreeze::visitEntry( - bool, - std::shared_ptr const&, - std::shared_ptr const& after) -{ - if (after && after->getType() == ltRIPPLE_STATE) - { - std::uint32_t const uFlags = after->getFieldU32(sfFlags); - bool const lowFreeze = uFlags & lsfLowFreeze; - bool const lowDeepFreeze = uFlags & lsfLowDeepFreeze; - - bool const highFreeze = uFlags & lsfHighFreeze; - bool const highDeepFreeze = uFlags & lsfHighDeepFreeze; - - deepFreezeWithoutFreeze_ = (lowDeepFreeze && !lowFreeze) || (highDeepFreeze && !highFreeze); - } -} - -bool -NoDeepFreezeTrustLinesWithoutFreeze::finalize( - STTx const&, - TER const, - XRPAmount const, - ReadView const&, - beast::Journal const& j) -{ - if (!deepFreezeWithoutFreeze_) - return true; - - JLOG(j.fatal()) << "Invariant failed: a trust line with deep freeze flag " - "without normal freeze was created"; - return false; -} - -//------------------------------------------------------------------------------ - -void -TransfersNotFrozen::visitEntry( - bool isDelete, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - /* - * A trust line freeze state alone doesn't determine if a transfer is - * frozen. The transfer must be examined "end-to-end" because both sides of - * the transfer may have different freeze states and freeze impact depends - * on the transfer direction. This is why first we need to track the - * transfers using IssuerChanges senders/receivers. - * - * Only in validateIssuerChanges, after we collected all changes can we - * determine if the transfer is valid. - */ - if (!isValidEntry(before, after)) - { - return; - } - - auto const balanceChange = calculateBalanceChange(before, after, isDelete); - if (balanceChange.signum() == 0) - { - return; - } - - recordBalanceChanges(after, balanceChange); -} - -bool -TransfersNotFrozen::finalize( - STTx const& tx, - TER const ter, - XRPAmount const fee, - ReadView const& view, - beast::Journal const& j) -{ - /* - * We check this invariant regardless of deep freeze amendment status, - * allowing for detection and logging of potential issues even when the - * amendment is disabled. - * - * If an exploit that allows moving frozen assets is discovered, - * we can alert operators who monitor fatal messages and trigger assert in - * debug builds for an early warning. - * - * In an unlikely event that an exploit is found, this early detection - * enables encouraging the UNL to expedite deep freeze amendment activation - * or deploy hotfixes via new amendments. In case of a new amendment, we'd - * only have to change this line setting 'enforce' variable. - * enforce = view.rules().enabled(featureDeepFreeze) || - * view.rules().enabled(fixFreezeExploit); - */ - [[maybe_unused]] bool const enforce = view.rules().enabled(featureDeepFreeze); - - for (auto const& [issue, changes] : balanceChanges_) - { - auto const issuerSle = findIssuer(issue.account, view); - // It should be impossible for the issuer to not be found, but check - // 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, - "xrpl::TransfersNotFrozen::finalize : enforce " - "invariant."); - if (enforce) - { - return false; - } - continue; - } - - if (!validateIssuerChanges(issuerSle, changes, tx, j, enforce)) - { - return false; - } - } - - return true; -} - -bool -TransfersNotFrozen::isValidEntry( - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - // `after` can never be null, even if the trust line is deleted. - XRPL_ASSERT(after, "xrpl::TransfersNotFrozen::isValidEntry : valid after."); - if (!after) - { - return false; - } - - if (after->getType() == ltACCOUNT_ROOT) - { - possibleIssuers_.emplace(after->at(sfAccount), after); - return false; - } - - /* While LedgerEntryTypesMatch invariant also checks types, all invariants - * are processed regardless of previous failures. - * - * This type check is still necessary here because it prevents potential - * issues in subsequent processing. - */ - return after->getType() == ltRIPPLE_STATE && (!before || before->getType() == ltRIPPLE_STATE); -} - -STAmount -TransfersNotFrozen::calculateBalanceChange( - std::shared_ptr const& before, - std::shared_ptr const& after, - bool isDelete) -{ - auto const getBalance = [](auto const& line, auto const& other, bool zero) { - STAmount amt = line ? line->at(sfBalance) : other->at(sfBalance).zeroed(); - return zero ? amt.zeroed() : amt; - }; - - /* Trust lines can be created dynamically by other transactions such as - * Payment and OfferCreate that cross offers. Such trust line won't be - * created frozen, but the sender might be, so the starting balance must be - * treated as zero. - */ - auto const balanceBefore = getBalance(before, after, false); - - /* Same as above, trust lines can be dynamically deleted, and for frozen - * trust lines, payments not involving the issuer must be blocked. This is - * achieved by treating the final balance as zero when isDelete=true to - * ensure frozen line restrictions are enforced even during deletion. - */ - auto const balanceAfter = getBalance(after, before, isDelete); - - return balanceAfter - balanceBefore; -} - -void -TransfersNotFrozen::recordBalance(Issue const& issue, BalanceChange change) -{ - XRPL_ASSERT( - change.balanceChangeSign, - "xrpl::TransfersNotFrozen::recordBalance : valid trustline " - "balance sign."); - auto& changes = balanceChanges_[issue]; - if (change.balanceChangeSign < 0) - changes.senders.emplace_back(std::move(change)); - else - changes.receivers.emplace_back(std::move(change)); -} - -void -TransfersNotFrozen::recordBalanceChanges( - std::shared_ptr const& after, - STAmount const& balanceChange) -{ - auto const balanceChangeSign = balanceChange.signum(); - auto const currency = after->at(sfBalance).getCurrency(); - - // Change from low account's perspective, which is trust line default - recordBalance({currency, after->at(sfHighLimit).getIssuer()}, {after, balanceChangeSign}); - - // Change from high account's perspective, which reverses the sign. - recordBalance({currency, after->at(sfLowLimit).getIssuer()}, {after, -balanceChangeSign}); -} - -std::shared_ptr -TransfersNotFrozen::findIssuer(AccountID const& issuerID, ReadView const& view) -{ - if (auto it = possibleIssuers_.find(issuerID); it != possibleIssuers_.end()) - { - return it->second; - } - - return view.read(keylet::account(issuerID)); -} - -bool -TransfersNotFrozen::validateIssuerChanges( - std::shared_ptr const& issuer, - IssuerChanges const& changes, - STTx const& tx, - beast::Journal const& j, - bool enforce) -{ - if (!issuer) - { - return false; - } - - bool const globalFreeze = issuer->isFlag(lsfGlobalFreeze); - if (changes.receivers.empty() || changes.senders.empty()) - { - /* If there are no receivers, then the holder(s) are returning - * their tokens to the issuer. Likewise, if there are no - * senders, then the issuer is issuing tokens to the holder(s). - * This is allowed regardless of the issuer's freeze flags. (The - * holder may have contradicting freeze flags, but that will be - * checked when the holder is treated as issuer.) - */ - return true; - } - - for (auto const& actors : {changes.senders, changes.receivers}) - { - for (auto const& change : actors) - { - bool const high = change.line->at(sfLowLimit).getIssuer() == issuer->at(sfAccount); - - if (!validateFrozenState(change, high, tx, j, enforce, globalFreeze)) - { - return false; - } - } - } - return true; -} - -bool -TransfersNotFrozen::validateFrozenState( - BalanceChange const& change, - bool high, - STTx const& tx, - beast::Journal const& j, - bool enforce, - bool globalFreeze) -{ - bool const freeze = - change.balanceChangeSign < 0 && change.line->isFlag(high ? lsfLowFreeze : lsfHighFreeze); - bool const deepFreeze = change.line->isFlag(high ? lsfLowDeepFreeze : lsfHighDeepFreeze); - bool const frozen = globalFreeze || deepFreeze || freeze; - - bool const isAMMLine = change.line->isFlag(lsfAMMNode); - - if (!frozen) - { - return true; - } - - // AMMClawbacks are allowed to override some freeze rules - if ((!isAMMLine || globalFreeze) && hasPrivilege(tx, overrideFreeze)) - { - JLOG(j.debug()) << "Invariant check allowing funds to be moved " - << (change.balanceChangeSign > 0 ? "to" : "from") - << " a frozen trustline for AMMClawback " << tx.getTransactionID(); - return true; - } - - 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, - "xrpl::TransfersNotFrozen::validateFrozenState : enforce " - "invariant."); - - if (enforce) - { - return false; - } - - return true; -} - -//------------------------------------------------------------------------------ - -void -ValidNewAccountRoot::visitEntry( - bool, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - if (!before && after->getType() == ltACCOUNT_ROOT) - { - accountsCreated_++; - accountSeq_ = (*after)[sfSequence]; - pseudoAccount_ = isPseudoAccount(after); - flags_ = after->getFlags(); - } -} - -bool -ValidNewAccountRoot::finalize( - STTx const& tx, - TER const result, - XRPAmount const, - ReadView const& view, - beast::Journal const& j) -{ - if (accountsCreated_ == 0) - return true; - - if (accountsCreated_ > 1) - { - JLOG(j.fatal()) << "Invariant failed: multiple accounts " - "created in a single transaction"; - return false; - } - - // From this point on we know exactly one account was created. - if (hasPrivilege(tx, createAcct | createPseudoAcct) && result == tesSUCCESS) - { - bool const pseudoAccount = - (pseudoAccount_ && - (view.rules().enabled(featureSingleAssetVault) || - view.rules().enabled(featureLendingProtocol))); - - if (pseudoAccount && !hasPrivilege(tx, createPseudoAcct)) - { - JLOG(j.fatal()) << "Invariant failed: pseudo-account created by a " - "wrong transaction type"; - return false; - } - - std::uint32_t const startingSeq = pseudoAccount ? 0 : view.seq(); - - if (accountSeq_ != startingSeq) - { - JLOG(j.fatal()) << "Invariant failed: account created with " - "wrong starting sequence number"; - return false; - } - - if (pseudoAccount) - { - std::uint32_t const expected = (lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth); - if (flags_ != expected) - { - JLOG(j.fatal()) << "Invariant failed: pseudo-account created with " - "wrong flags"; - return false; - } - } - - return true; - } - - JLOG(j.fatal()) << "Invariant failed: account root created illegally"; - return false; -} // namespace xrpl - -//------------------------------------------------------------------------------ - -void -ValidNFTokenPage::visitEntry( - bool isDelete, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - static constexpr uint256 const& pageBits = nft::pageMask; - static constexpr uint256 const accountBits = ~pageBits; - - if ((before && before->getType() != ltNFTOKEN_PAGE) || - (after && after->getType() != ltNFTOKEN_PAGE)) - return; - - auto check = [this, isDelete](std::shared_ptr const& sle) { - uint256 const account = sle->key() & accountBits; - uint256 const hiLimit = sle->key() & pageBits; - std::optional const prev = (*sle)[~sfPreviousPageMin]; - - // Make sure that any page links... - // 1. Are properly associated with the owning account and - // 2. The page is correctly ordered between links. - if (prev) - { - if (account != (*prev & accountBits)) - badLink_ = true; - - if (hiLimit <= (*prev & pageBits)) - badLink_ = true; - } - - if (auto const next = (*sle)[~sfNextPageMin]) - { - if (account != (*next & accountBits)) - badLink_ = true; - - if (hiLimit >= (*next & pageBits)) - badLink_ = true; - } - - { - auto const& nftokens = sle->getFieldArray(sfNFTokens); - - // An NFTokenPage should never contain too many tokens or be empty. - if (std::size_t const nftokenCount = nftokens.size(); - (!isDelete && nftokenCount == 0) || nftokenCount > dirMaxTokensPerPage) - invalidSize_ = true; - - // If prev is valid, use it to establish a lower bound for - // page entries. If prev is not valid the lower bound is zero. - uint256 const loLimit = prev ? *prev & pageBits : uint256(beast::zero); - - // Also verify that all NFTokenIDs in the page are sorted. - uint256 loCmp = loLimit; - for (auto const& obj : nftokens) - { - uint256 const tokenID = obj[sfNFTokenID]; - if (!nft::compareTokens(loCmp, tokenID)) - badSort_ = true; - loCmp = tokenID; - - // None of the NFTs on this page should belong on lower or - // higher pages. - if (uint256 const tokenPageBits = tokenID & pageBits; - tokenPageBits < loLimit || tokenPageBits >= hiLimit) - badEntry_ = true; - - if (auto uri = obj[~sfURI]; uri && uri->empty()) - badURI_ = true; - } - } - }; - - if (before) - { - check(before); - - // While an account's NFToken directory contains any NFTokens, the last - // NFTokenPage (with 96 bits of 1 in the low part of the index) should - // never be deleted. - if (isDelete && (before->key() & nft::pageMask) == nft::pageMask && - before->isFieldPresent(sfPreviousPageMin)) - { - deletedFinalPage_ = true; - } - } - - if (after) - check(after); - - if (!isDelete && before && after) - { - // If the NFTokenPage - // 1. Has a NextMinPage field in before, but loses it in after, and - // 2. This is not the last page in the directory - // Then we have identified a corruption in the links between the - // NFToken pages in the NFToken directory. - if ((before->key() & nft::pageMask) != nft::pageMask && - before->isFieldPresent(sfNextPageMin) && !after->isFieldPresent(sfNextPageMin)) - { - deletedLink_ = true; - } - } -} - -bool -ValidNFTokenPage::finalize( - STTx const& tx, - TER const result, - XRPAmount const, - ReadView const& view, - beast::Journal const& j) -{ - if (badLink_) - { - JLOG(j.fatal()) << "Invariant failed: NFT page is improperly linked."; - return false; - } - - if (badEntry_) - { - JLOG(j.fatal()) << "Invariant failed: NFT found in incorrect page."; - return false; - } - - if (badSort_) - { - JLOG(j.fatal()) << "Invariant failed: NFTs on page are not sorted."; - return false; - } - - if (badURI_) - { - JLOG(j.fatal()) << "Invariant failed: NFT contains empty URI."; - return false; - } - - if (invalidSize_) - { - JLOG(j.fatal()) << "Invariant failed: NFT page has invalid size."; - return false; - } - - if (view.rules().enabled(fixNFTokenPageLinks)) - { - if (deletedFinalPage_) - { - JLOG(j.fatal()) << "Invariant failed: Last NFT page deleted with " - "non-empty directory."; - return false; - } - if (deletedLink_) - { - JLOG(j.fatal()) << "Invariant failed: Lost NextMinPage link."; - return false; - } - } - - return true; -} - -//------------------------------------------------------------------------------ -void -NFTokenCountTracking::visitEntry( - bool, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - if (before && before->getType() == ltACCOUNT_ROOT) - { - beforeMintedTotal += (*before)[~sfMintedNFTokens].value_or(0); - beforeBurnedTotal += (*before)[~sfBurnedNFTokens].value_or(0); - } - - if (after && after->getType() == ltACCOUNT_ROOT) - { - afterMintedTotal += (*after)[~sfMintedNFTokens].value_or(0); - afterBurnedTotal += (*after)[~sfBurnedNFTokens].value_or(0); - } -} - -bool -NFTokenCountTracking::finalize( - STTx const& tx, - TER const result, - XRPAmount const, - ReadView const& view, - beast::Journal const& j) -{ - if (!hasPrivilege(tx, changeNFTCounts)) - { - if (beforeMintedTotal != afterMintedTotal) - { - JLOG(j.fatal()) << "Invariant failed: the number of minted tokens " - "changed without a mint transaction!"; - return false; - } - - if (beforeBurnedTotal != afterBurnedTotal) - { - JLOG(j.fatal()) << "Invariant failed: the number of burned tokens " - "changed without a burn transaction!"; - return false; - } - - return true; - } - - if (tx.getTxnType() == ttNFTOKEN_MINT) - { - if (result == tesSUCCESS && beforeMintedTotal >= afterMintedTotal) - { - JLOG(j.fatal()) << "Invariant failed: successful minting didn't increase " - "the number of minted tokens."; - return false; - } - - if (result != tesSUCCESS && beforeMintedTotal != afterMintedTotal) - { - JLOG(j.fatal()) << "Invariant failed: failed minting changed the " - "number of minted tokens."; - return false; - } - - if (beforeBurnedTotal != afterBurnedTotal) - { - JLOG(j.fatal()) << "Invariant failed: minting changed the number of " - "burned tokens."; - return false; - } - } - - if (tx.getTxnType() == ttNFTOKEN_BURN) - { - if (result == tesSUCCESS) - { - if (beforeBurnedTotal >= afterBurnedTotal) - { - JLOG(j.fatal()) << "Invariant failed: successful burning didn't increase " - "the number of burned tokens."; - return false; - } - } - - if (result != tesSUCCESS && beforeBurnedTotal != afterBurnedTotal) - { - JLOG(j.fatal()) << "Invariant failed: failed burning changed the " - "number of burned tokens."; - return false; - } - - if (beforeMintedTotal != afterMintedTotal) - { - JLOG(j.fatal()) << "Invariant failed: burning changed the number of " - "minted tokens."; - return false; - } - } - - return true; -} - -//------------------------------------------------------------------------------ - -void -ValidClawback::visitEntry( - bool, - std::shared_ptr const& before, - std::shared_ptr const&) -{ - if (before && before->getType() == ltRIPPLE_STATE) - trustlinesChanged++; - - if (before && before->getType() == ltMPTOKEN) - mptokensChanged++; -} - -bool -ValidClawback::finalize( - STTx const& tx, - TER const result, - XRPAmount const, - ReadView const& view, - beast::Journal const& j) -{ - if (tx.getTxnType() != ttCLAWBACK) - return true; - - if (result == tesSUCCESS) - { - if (trustlinesChanged > 1) - { - JLOG(j.fatal()) << "Invariant failed: more than one trustline changed."; - return false; - } - - if (mptokensChanged > 1) - { - JLOG(j.fatal()) << "Invariant failed: more than one mptokens changed."; - return false; - } - - if (trustlinesChanged == 1) - { - AccountID const issuer = tx.getAccountID(sfAccount); - STAmount const& amount = tx.getFieldAmount(sfAmount); - AccountID const& holder = amount.getIssuer(); - STAmount const holderBalance = - accountHolds(view, holder, amount.getCurrency(), issuer, fhIGNORE_FREEZE, j); - - if (holderBalance.signum() < 0) - { - JLOG(j.fatal()) << "Invariant failed: trustline balance is negative"; - return false; - } - } - } - else - { - if (trustlinesChanged != 0) - { - JLOG(j.fatal()) << "Invariant failed: some trustlines were changed " - "despite failure of the transaction."; - return false; - } - - if (mptokensChanged != 0) - { - JLOG(j.fatal()) << "Invariant failed: some mptokens were changed " - "despite failure of the transaction."; - return false; - } - } - - return true; -} - -//------------------------------------------------------------------------------ - -void -ValidMPTIssuance::visitEntry( - bool isDelete, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - if (after && after->getType() == ltMPTOKEN_ISSUANCE) - { - if (isDelete) - mptIssuancesDeleted_++; - else if (!before) - mptIssuancesCreated_++; - } - - if (after && after->getType() == ltMPTOKEN) - { - if (isDelete) - mptokensDeleted_++; - else if (!before) - { - mptokensCreated_++; - MPTIssue const mptIssue{after->at(sfMPTokenIssuanceID)}; - if (mptIssue.getIssuer() == after->at(sfAccount)) - mptCreatedByIssuer_ = true; - } - } -} - -bool -ValidMPTIssuance::finalize( - STTx const& tx, - TER const result, - XRPAmount const _fee, - ReadView const& view, - beast::Journal const& j) -{ - if (result == tesSUCCESS) - { - auto const& rules = view.rules(); - [[maybe_unused]] - bool enforceCreatedByIssuer = - rules.enabled(featureSingleAssetVault) || rules.enabled(featureLendingProtocol); - if (mptCreatedByIssuer_) - { - JLOG(j.fatal()) << "Invariant failed: MPToken created for the MPT issuer"; - // The comment above starting with "assert(enforce)" explains this - // assert. - XRPL_ASSERT_PARTS( - enforceCreatedByIssuer, "xrpl::ValidMPTIssuance::finalize", "no issuer MPToken"); - if (enforceCreatedByIssuer) - return false; - } - - auto const txnType = tx.getTxnType(); - if (hasPrivilege(tx, createMPTIssuance)) - { - if (mptIssuancesCreated_ == 0) - { - JLOG(j.fatal()) << "Invariant failed: transaction " - "succeeded without creating a MPT issuance"; - } - else if (mptIssuancesDeleted_ != 0) - { - JLOG(j.fatal()) << "Invariant failed: transaction " - "succeeded while removing MPT issuances"; - } - else if (mptIssuancesCreated_ > 1) - { - JLOG(j.fatal()) << "Invariant failed: transaction " - "succeeded but created multiple issuances"; - } - - return mptIssuancesCreated_ == 1 && mptIssuancesDeleted_ == 0; - } - - if (hasPrivilege(tx, destroyMPTIssuance)) - { - if (mptIssuancesDeleted_ == 0) - { - JLOG(j.fatal()) << "Invariant failed: MPT issuance deletion " - "succeeded without removing a MPT issuance"; - } - else if (mptIssuancesCreated_ > 0) - { - JLOG(j.fatal()) << "Invariant failed: MPT issuance deletion " - "succeeded while creating MPT issuances"; - } - else if (mptIssuancesDeleted_ > 1) - { - JLOG(j.fatal()) << "Invariant failed: MPT issuance deletion " - "succeeded but deleted multiple issuances"; - } - - return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 1; - } - - bool const lendingProtocolEnabled = view.rules().enabled(featureLendingProtocol); - // 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 = (txnType == ttESCROW_FINISH) && - (view.rules().enabled(featureSingleAssetVault) || lendingProtocolEnabled); - if (hasPrivilege(tx, mustAuthorizeMPT | mayAuthorizeMPT) || enforceEscrowFinish) - { - bool const submittedByIssuer = tx.isFieldPresent(sfHolder); - - if (mptIssuancesCreated_ > 0) - { - JLOG(j.fatal()) << "Invariant failed: MPT authorize " - "succeeded but created MPT issuances"; - return false; - } - else if (mptIssuancesDeleted_ > 0) - { - JLOG(j.fatal()) << "Invariant failed: MPT authorize " - "succeeded but deleted issuances"; - return false; - } - else if (lendingProtocolEnabled && mptokensCreated_ + mptokensDeleted_ > 1) - { - JLOG(j.fatal()) << "Invariant failed: MPT authorize succeeded " - "but created/deleted bad number mptokens"; - return false; - } - else if (submittedByIssuer && (mptokensCreated_ > 0 || mptokensDeleted_ > 0)) - { - JLOG(j.fatal()) << "Invariant failed: MPT authorize submitted by issuer " - "succeeded but created/deleted mptokens"; - return false; - } - else if ( - !submittedByIssuer && hasPrivilege(tx, mustAuthorizeMPT) && - (mptokensCreated_ + mptokensDeleted_ != 1)) - { - // if the holder submitted this tx, then a mptoken must be - // either created or deleted. - JLOG(j.fatal()) << "Invariant failed: MPT authorize submitted by holder " - "succeeded but created/deleted bad number of mptokens"; - return false; - } - - return true; - } - if (txnType == ttESCROW_FINISH) - { - // 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, "xrpl::ValidMPTIssuance::finalize", "not escrow finish tx"); - return true; - } - - if (hasPrivilege(tx, mayDeleteMPT) && mptokensDeleted_ == 1 && mptokensCreated_ == 0 && - mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0) - return true; - } - - if (mptIssuancesCreated_ != 0) - { - JLOG(j.fatal()) << "Invariant failed: a MPT issuance was created"; - } - else if (mptIssuancesDeleted_ != 0) - { - JLOG(j.fatal()) << "Invariant failed: a MPT issuance was deleted"; - } - else if (mptokensCreated_ != 0) - { - JLOG(j.fatal()) << "Invariant failed: a MPToken was created"; - } - else if (mptokensDeleted_ != 0) - { - JLOG(j.fatal()) << "Invariant failed: a MPToken was deleted"; - } - - return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0 && mptokensCreated_ == 0 && - mptokensDeleted_ == 0; -} - -//------------------------------------------------------------------------------ - -void -ValidPermissionedDomain::visitEntry( - bool isDel, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - if (before && before->getType() != ltPERMISSIONED_DOMAIN) - return; - if (after && after->getType() != ltPERMISSIONED_DOMAIN) - return; - - auto check = [isDel](std::vector& sleStatus, std::shared_ptr const& sle) { - auto const& credentials = sle->getFieldArray(sfAcceptedCredentials); - auto const sorted = credentials::makeSorted(credentials); - - SleStatus ss{credentials.size(), false, !sorted.empty(), isDel}; - - // If array have duplicates then all the other checks are invalid - if (ss.isUnique_) - { - unsigned i = 0; - for (auto const& cred : sorted) - { - auto const& credTx = credentials[i++]; - ss.isSorted_ = - (cred.first == credTx[sfIssuer]) && (cred.second == credTx[sfCredentialType]); - if (!ss.isSorted_) - break; - } - } - sleStatus.emplace_back(std::move(ss)); - }; - - if (after) - check(sleStatus_, after); -} - -bool -ValidPermissionedDomain::finalize( - STTx const& tx, - TER const result, - XRPAmount const, - ReadView const& view, - beast::Journal const& j) -{ - auto check = [](SleStatus const& sleStatus, beast::Journal const& j) { - if (!sleStatus.credentialsSize_) - { - JLOG(j.fatal()) << "Invariant failed: permissioned domain with " - "no rules."; - return false; - } - - if (sleStatus.credentialsSize_ > maxPermissionedDomainCredentialsArraySize) - { - JLOG(j.fatal()) << "Invariant failed: permissioned domain bad " - "credentials size " - << sleStatus.credentialsSize_; - return false; - } - - if (!sleStatus.isUnique_) - { - JLOG(j.fatal()) << "Invariant failed: permissioned domain credentials " - "aren't unique"; - return false; - } - - if (!sleStatus.isSorted_) - { - JLOG(j.fatal()) << "Invariant failed: permissioned domain credentials " - "aren't sorted"; - return false; - } - - return true; - }; - - if (view.rules().enabled(fixPermissionedDomainInvariant)) - { - // No permissioned domains should be affected if the transaction failed - if (result != tesSUCCESS) - // If nothing changed, all is good. If there were changes, that's - // bad. - return sleStatus_.empty(); - - if (sleStatus_.size() > 1) - { - JLOG(j.fatal()) << "Invariant failed: transaction affected more " - "than 1 permissioned domain entry."; - return false; - } - - switch (tx.getTxnType()) - { - case ttPERMISSIONED_DOMAIN_SET: { - if (sleStatus_.empty()) - { - JLOG(j.fatal()) << "Invariant failed: no domain objects affected by " - "PermissionedDomainSet"; - return false; - } - - auto const& sleStatus = sleStatus_[0]; - if (sleStatus.isDelete_) - { - JLOG(j.fatal()) << "Invariant failed: domain object " - "deleted by PermissionedDomainSet"; - return false; - } - return check(sleStatus, j); - } - case ttPERMISSIONED_DOMAIN_DELETE: { - if (sleStatus_.empty()) - { - JLOG(j.fatal()) << "Invariant failed: no domain objects affected by " - "PermissionedDomainDelete"; - return false; - } - - if (!sleStatus_[0].isDelete_) - { - JLOG(j.fatal()) << "Invariant failed: domain object " - "modified, but not deleted by " - "PermissionedDomainDelete"; - return false; - } - return true; - } - default: { - if (!sleStatus_.empty()) - { - JLOG(j.fatal()) << "Invariant failed: " << sleStatus_.size() - << " domain object(s) affected by an " - "unauthorized transaction. " - << tx.getTxnType(); - return false; - } - return true; - } - } - } - else - { - if (tx.getTxnType() != ttPERMISSIONED_DOMAIN_SET || result != tesSUCCESS || - sleStatus_.empty()) - return true; - return check(sleStatus_[0], j); - } -} - -//------------------------------------------------------------------------------ - -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); - XRPL_ASSERT( - errors_.empty() || enforce, - "xrpl::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, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - if (after && after->getType() == ltDIR_NODE) - { - if (after->isFieldPresent(sfDomainID)) - domains_.insert(after->getFieldH256(sfDomainID)); - } - - if (after && after->getType() == ltOFFER) - { - if (after->isFieldPresent(sfDomainID)) - domains_.insert(after->getFieldH256(sfDomainID)); - else - regularOffers_ = true; - - // if a hybrid offer is missing domain or additional book, there's - // something wrong - if (after->isFlag(lsfHybrid) && - (!after->isFieldPresent(sfDomainID) || !after->isFieldPresent(sfAdditionalBooks) || - after->getFieldArray(sfAdditionalBooks).size() > 1)) - badHybrids_ = true; - } -} - -bool -ValidPermissionedDEX::finalize( - STTx const& tx, - TER const result, - XRPAmount const, - ReadView const& view, - beast::Journal const& j) -{ - auto const txType = tx.getTxnType(); - if ((txType != ttPAYMENT && txType != ttOFFER_CREATE) || result != tesSUCCESS) - return true; - - // For each offercreate transaction, check if - // permissioned offers are valid - if (txType == ttOFFER_CREATE && badHybrids_) - { - JLOG(j.fatal()) << "Invariant failed: hybrid offer is malformed"; - return false; - } - - if (!tx.isFieldPresent(sfDomainID)) - return true; - - auto const domain = tx.getFieldH256(sfDomainID); - - if (!view.exists(keylet::permissionedDomain(domain))) - { - JLOG(j.fatal()) << "Invariant failed: domain doesn't exist"; - return false; - } - - // for both payment and offercreate, there shouldn't be another domain - // that's different from the domain specified - for (auto const& d : domains_) - { - if (d != domain) - { - JLOG(j.fatal()) << "Invariant failed: transaction" - " consumed wrong domains"; - return false; - } - } - - if (regularOffers_) - { - JLOG(j.fatal()) << "Invariant failed: domain transaction" - " affected regular offers"; - return false; - } - - return true; -} - -void -ValidAMM::visitEntry( - bool isDelete, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - if (isDelete) - return; - - if (after) - { - auto const type = after->getType(); - // AMM object changed - if (type == ltAMM) - { - ammAccount_ = after->getAccountID(sfAccount); - lptAMMBalanceAfter_ = after->getFieldAmount(sfLPTokenBalance); - } - // AMM pool changed - else if ( - (type == ltRIPPLE_STATE && after->getFlags() & lsfAMMNode) || - (type == ltACCOUNT_ROOT && after->isFieldPresent(sfAMMID))) - { - ammPoolChanged_ = true; - } - } - - if (before) - { - // AMM object changed - if (before->getType() == ltAMM) - { - lptAMMBalanceBefore_ = before->getFieldAmount(sfLPTokenBalance); - } - } -} - -static bool -validBalances( - STAmount const& amount, - STAmount const& amount2, - STAmount const& lptAMMBalance, - ValidAMM::ZeroAllowed zeroAllowed) -{ - bool const positive = - amount > beast::zero && amount2 > beast::zero && lptAMMBalance > beast::zero; - if (zeroAllowed == ValidAMM::ZeroAllowed::Yes) - return positive || - (amount == beast::zero && amount2 == beast::zero && lptAMMBalance == beast::zero); - return positive; -} - -bool -ValidAMM::finalizeVote(bool enforce, beast::Journal const& j) const -{ - if (lptAMMBalanceAfter_ != lptAMMBalanceBefore_ || ammPoolChanged_) - { - // LPTokens and the pool can not change on vote - // LCOV_EXCL_START - JLOG(j.error()) << "AMMVote invariant failed: " << lptAMMBalanceBefore_.value_or(STAmount{}) - << " " << lptAMMBalanceAfter_.value_or(STAmount{}) << " " - << ammPoolChanged_; - if (enforce) - return false; - // LCOV_EXCL_STOP - } - - return true; -} - -bool -ValidAMM::finalizeBid(bool enforce, beast::Journal const& j) const -{ - if (ammPoolChanged_) - { - // The pool can not change on bid - // LCOV_EXCL_START - JLOG(j.error()) << "AMMBid invariant failed: pool changed"; - if (enforce) - return false; - // LCOV_EXCL_STOP - } - // LPTokens are burnt, therefore there should be fewer LPTokens - else if ( - lptAMMBalanceBefore_ && lptAMMBalanceAfter_ && - (*lptAMMBalanceAfter_ > *lptAMMBalanceBefore_ || *lptAMMBalanceAfter_ <= beast::zero)) - { - // LCOV_EXCL_START - JLOG(j.error()) << "AMMBid invariant failed: " << *lptAMMBalanceBefore_ << " " - << *lptAMMBalanceAfter_; - if (enforce) - return false; - // LCOV_EXCL_STOP - } - - return true; -} - -bool -ValidAMM::finalizeCreate( - STTx const& tx, - ReadView const& view, - bool enforce, - beast::Journal const& j) const -{ - if (!ammAccount_) - { - // LCOV_EXCL_START - JLOG(j.error()) << "AMMCreate invariant failed: AMM object is not created"; - if (enforce) - return false; - // LCOV_EXCL_STOP - } - else - { - auto const [amount, amount2] = ammPoolHolds( - view, - *ammAccount_, - tx[sfAmount].get(), - tx[sfAmount2].get(), - fhIGNORE_FREEZE, - j); - // Create invariant: - // sqrt(amount * amount2) == LPTokens - // all balances are greater than zero - if (!validBalances(amount, amount2, *lptAMMBalanceAfter_, ZeroAllowed::No) || - ammLPTokens(amount, amount2, lptAMMBalanceAfter_->issue()) != *lptAMMBalanceAfter_) - { - JLOG(j.error()) << "AMMCreate invariant failed: " << amount << " " << amount2 << " " - << *lptAMMBalanceAfter_; - if (enforce) - return false; - } - } - - return true; -} - -bool -ValidAMM::finalizeDelete(bool enforce, TER res, beast::Journal const& j) const -{ - if (ammAccount_) - { - // LCOV_EXCL_START - std::string const msg = (res == tesSUCCESS) ? "AMM object is not deleted on tesSUCCESS" - : "AMM object is changed on tecINCOMPLETE"; - JLOG(j.error()) << "AMMDelete invariant failed: " << msg; - if (enforce) - return false; - // LCOV_EXCL_STOP - } - - return true; -} - -bool -ValidAMM::finalizeDEX(bool enforce, beast::Journal const& j) const -{ - if (ammAccount_) - { - // LCOV_EXCL_START - JLOG(j.error()) << "AMM swap invariant failed: AMM object changed"; - if (enforce) - return false; - // LCOV_EXCL_STOP - } - - return true; -} - -bool -ValidAMM::generalInvariant( - xrpl::STTx const& tx, - xrpl::ReadView const& view, - ZeroAllowed zeroAllowed, - beast::Journal const& j) const -{ - auto const [amount, amount2] = ammPoolHolds( - view, - *ammAccount_, - tx[sfAsset].get(), - tx[sfAsset2].get(), - fhIGNORE_FREEZE, - j); - // Deposit and Withdrawal invariant: - // sqrt(amount * amount2) >= LPTokens - // all balances are greater than zero - // unless on last withdrawal - auto const poolProductMean = root2(amount * amount2); - bool const nonNegativeBalances = - validBalances(amount, amount2, *lptAMMBalanceAfter_, zeroAllowed); - bool const strongInvariantCheck = poolProductMean >= *lptAMMBalanceAfter_; - // Allow for a small relative error if strongInvariantCheck fails - auto weakInvariantCheck = [&]() { - return *lptAMMBalanceAfter_ != beast::zero && - withinRelativeDistance(poolProductMean, Number{*lptAMMBalanceAfter_}, Number{1, -11}); - }; - if (!nonNegativeBalances || (!strongInvariantCheck && !weakInvariantCheck())) - { - JLOG(j.error()) << "AMM " << tx.getTxnType() - << " invariant failed: " << tx.getHash(HashPrefix::transactionID) << " " - << ammPoolChanged_ << " " << amount << " " << amount2 << " " - << poolProductMean << " " << lptAMMBalanceAfter_->getText() << " " - << ((*lptAMMBalanceAfter_ == beast::zero) - ? Number{1} - : ((*lptAMMBalanceAfter_ - poolProductMean) / poolProductMean)); - return false; - } - - return true; -} - -bool -ValidAMM::finalizeDeposit( - xrpl::STTx const& tx, - xrpl::ReadView const& view, - bool enforce, - beast::Journal const& j) const -{ - if (!ammAccount_) - { - // LCOV_EXCL_START - JLOG(j.error()) << "AMMDeposit invariant failed: AMM object is deleted"; - if (enforce) - return false; - // LCOV_EXCL_STOP - } - else if (!generalInvariant(tx, view, ZeroAllowed::No, j) && enforce) - return false; - - return true; -} - -bool -ValidAMM::finalizeWithdraw( - xrpl::STTx const& tx, - xrpl::ReadView const& view, - bool enforce, - beast::Journal const& j) const -{ - if (!ammAccount_) - { - // Last Withdraw or Clawback deleted AMM - } - else if (!generalInvariant(tx, view, ZeroAllowed::Yes, j)) - { - if (enforce) - return false; - } - - return true; -} - -bool -ValidAMM::finalize( - STTx const& tx, - TER const result, - XRPAmount const, - ReadView const& view, - beast::Journal const& j) -{ - // Delete may return tecINCOMPLETE if there are too many - // trustlines to delete. - if (result != tesSUCCESS && result != tecINCOMPLETE) - return true; - - bool const enforce = view.rules().enabled(fixAMMv1_3); - - switch (tx.getTxnType()) - { - case ttAMM_CREATE: - return finalizeCreate(tx, view, enforce, j); - case ttAMM_DEPOSIT: - return finalizeDeposit(tx, view, enforce, j); - case ttAMM_CLAWBACK: - case ttAMM_WITHDRAW: - return finalizeWithdraw(tx, view, enforce, j); - case ttAMM_BID: - return finalizeBid(enforce, j); - case ttAMM_VOTE: - return finalizeVote(enforce, j); - case ttAMM_DELETE: - return finalizeDelete(enforce, result, j); - case ttCHECK_CASH: - case ttOFFER_CREATE: - case ttPAYMENT: - return finalizeDEX(enforce, j); - default: - break; - } - - return true; -} - -//------------------------------------------------------------------------------ - -void -NoModifiedUnmodifiableFields::visitEntry( - bool isDelete, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - if (isDelete || !before) - // Creation and deletion are ignored - return; - - changedEntries_.emplace(before, after); -} - -bool -NoModifiedUnmodifiableFields::finalize( - STTx const& tx, - TER const, - XRPAmount const, - ReadView const& view, - beast::Journal const& j) -{ - static auto const fieldChanged = [](auto const& before, auto const& after, auto const& field) { - bool const beforeField = before->isFieldPresent(field); - bool const afterField = after->isFieldPresent(field); - return beforeField != afterField || (afterField && before->at(field) != after->at(field)); - }; - for (auto const& slePair : changedEntries_) - { - auto const& before = slePair.first; - auto const& after = slePair.second; - auto const type = after->getType(); - bool bad = false; - [[maybe_unused]] bool enforce = false; - switch (type) - { - case ltLOAN_BROKER: - /* - * We check this invariant regardless of lending protocol - * amendment status, allowing for detection and logging of - * potential issues even when the amendment is disabled. - */ - enforce = view.rules().enabled(featureLendingProtocol); - bad = fieldChanged(before, after, sfLedgerEntryType) || - fieldChanged(before, after, sfLedgerIndex) || - fieldChanged(before, after, sfSequence) || - fieldChanged(before, after, sfOwnerNode) || - fieldChanged(before, after, sfVaultNode) || - fieldChanged(before, after, sfVaultID) || - fieldChanged(before, after, sfAccount) || - fieldChanged(before, after, sfOwner) || - fieldChanged(before, after, sfManagementFeeRate) || - fieldChanged(before, after, sfCoverRateMinimum) || - fieldChanged(before, after, sfCoverRateLiquidation); - break; - case ltLOAN: - /* - * We check this invariant regardless of lending protocol - * amendment status, allowing for detection and logging of - * potential issues even when the amendment is disabled. - */ - enforce = view.rules().enabled(featureLendingProtocol); - bad = fieldChanged(before, after, sfLedgerEntryType) || - fieldChanged(before, after, sfLedgerIndex) || - fieldChanged(before, after, sfSequence) || - fieldChanged(before, after, sfOwnerNode) || - fieldChanged(before, after, sfLoanBrokerNode) || - fieldChanged(before, after, sfLoanBrokerID) || - fieldChanged(before, after, sfBorrower) || - fieldChanged(before, after, sfLoanOriginationFee) || - fieldChanged(before, after, sfLoanServiceFee) || - fieldChanged(before, after, sfLatePaymentFee) || - fieldChanged(before, after, sfClosePaymentFee) || - fieldChanged(before, after, sfOverpaymentFee) || - fieldChanged(before, after, sfInterestRate) || - fieldChanged(before, after, sfLateInterestRate) || - fieldChanged(before, after, sfCloseInterestRate) || - fieldChanged(before, after, sfOverpaymentInterestRate) || - fieldChanged(before, after, sfStartDate) || - fieldChanged(before, after, sfPaymentInterval) || - fieldChanged(before, after, sfGracePeriod) || - fieldChanged(before, after, sfLoanScale); - break; - default: - /* - * We check this invariant regardless of lending protocol - * amendment status, allowing for detection and logging of - * potential issues even when the amendment is disabled. - * - * We use the lending protocol as a gate, even though - * all transactions are affected because that's when it - * was added. - */ - enforce = view.rules().enabled(featureLendingProtocol); - bad = fieldChanged(before, after, sfLedgerEntryType) || - fieldChanged(before, after, sfLedgerIndex); - } - XRPL_ASSERT( - !bad || enforce, - "xrpl::NoModifiedUnmodifiableFields::finalize : no bad " - "changes or enforce invariant"); - if (bad) - { - JLOG(j.fatal()) << "Invariant failed: changed an unchangeable field for " - << tx.getTransactionID(); - if (enforce) - return false; - } - } - return true; -} - -//------------------------------------------------------------------------------ - -void -ValidLoanBroker::visitEntry( - bool isDelete, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - if (after) - { - if (after->getType() == ltLOAN_BROKER) - { - auto& broker = brokers_[after->key()]; - broker.brokerBefore = before; - broker.brokerAfter = after; - } - else if (after->getType() == ltACCOUNT_ROOT && after->isFieldPresent(sfLoanBrokerID)) - { - auto const& loanBrokerID = after->at(sfLoanBrokerID); - // create an entry if one doesn't already exist - brokers_.emplace(loanBrokerID, BrokerInfo{}); - } - else if (after->getType() == ltRIPPLE_STATE) - { - lines_.emplace_back(after); - } - else if (after->getType() == ltMPTOKEN) - { - mpts_.emplace_back(after); - } - } -} - -bool -ValidLoanBroker::goodZeroDirectory( - ReadView const& view, - SLE::const_ref dir, - beast::Journal const& j) const -{ - auto const next = dir->at(~sfIndexNext); - auto const prev = dir->at(~sfIndexPrevious); - if ((prev && *prev) || (next && *next)) - { - JLOG(j.fatal()) << "Invariant failed: Loan Broker with zero " - "OwnerCount has multiple directory pages"; - return false; - } - auto indexes = dir->getFieldV256(sfIndexes); - if (indexes.size() > 1) - { - JLOG(j.fatal()) << "Invariant failed: Loan Broker with zero " - "OwnerCount has multiple indexes in the Directory root"; - return false; - } - if (indexes.size() == 1) - { - auto const index = indexes.value().front(); - auto const sle = view.read(keylet::unchecked(index)); - if (!sle) - { - JLOG(j.fatal()) << "Invariant failed: Loan Broker directory corrupt"; - return false; - } - if (sle->getType() != ltRIPPLE_STATE && sle->getType() != ltMPTOKEN) - { - JLOG(j.fatal()) << "Invariant failed: Loan Broker with zero " - "OwnerCount has an unexpected entry in the directory"; - return false; - } - } - - return true; -} - -bool -ValidLoanBroker::finalize( - STTx const& tx, - TER const, - XRPAmount const, - ReadView const& view, - beast::Journal const& j) -{ - // Loan Brokers will not exist on ledger if the Lending Protocol amendment - // is not enabled, so there's no need to check it. - - for (auto const& line : lines_) - { - for (auto const& field : {&sfLowLimit, &sfHighLimit}) - { - auto const account = view.read(keylet::account(line->at(*field).getIssuer())); - // This Invariant doesn't know about the rules for Trust Lines, so - // if the account is missing, don't treat it as an error. This - // loop is only concerned with finding Broker pseudo-accounts - if (account && account->isFieldPresent(sfLoanBrokerID)) - { - auto const& loanBrokerID = account->at(sfLoanBrokerID); - // create an entry if one doesn't already exist - brokers_.emplace(loanBrokerID, BrokerInfo{}); - } - } - } - for (auto const& mpt : mpts_) - { - auto const account = view.read(keylet::account(mpt->at(sfAccount))); - // This Invariant doesn't know about the rules for MPTokens, so - // if the account is missing, don't treat is as an error. This - // loop is only concerned with finding Broker pseudo-accounts - if (account && account->isFieldPresent(sfLoanBrokerID)) - { - auto const& loanBrokerID = account->at(sfLoanBrokerID); - // create an entry if one doesn't already exist - brokers_.emplace(loanBrokerID, BrokerInfo{}); - } - } - - for (auto const& [brokerID, broker] : brokers_) - { - auto const& after = - broker.brokerAfter ? broker.brokerAfter : view.read(keylet::loanbroker(brokerID)); - - if (!after) - { - JLOG(j.fatal()) << "Invariant failed: Loan Broker missing"; - return false; - } - - auto const& before = broker.brokerBefore; - - // https://github.com/Tapanito/XRPL-Standards/blob/xls-66-lending-protocol/XLS-0066d-lending-protocol/README.md#3123-invariants - // If `LoanBroker.OwnerCount = 0` the `DirectoryNode` will have at most - // one node (the root), which will only hold entries for `RippleState` - // or `MPToken` objects. - if (after->at(sfOwnerCount) == 0) - { - auto const dir = view.read(keylet::ownerDir(after->at(sfAccount))); - if (dir) - { - if (!goodZeroDirectory(view, dir, j)) - { - return false; - } - } - } - if (before && before->at(sfLoanSequence) > after->at(sfLoanSequence)) - { - JLOG(j.fatal()) << "Invariant failed: Loan Broker sequence number " - "decreased"; - return false; - } - if (after->at(sfDebtTotal) < 0) - { - JLOG(j.fatal()) << "Invariant failed: Loan Broker debt total is negative"; - return false; - } - if (after->at(sfCoverAvailable) < 0) - { - JLOG(j.fatal()) << "Invariant failed: Loan Broker cover available is negative"; - return false; - } - auto const vault = view.read(keylet::vault(after->at(sfVaultID))); - if (!vault) - { - JLOG(j.fatal()) << "Invariant failed: Loan Broker vault ID is invalid"; - return false; - } - auto const& vaultAsset = vault->at(sfAsset); - if (after->at(sfCoverAvailable) < accountHolds( - view, - after->at(sfAccount), - vaultAsset, - FreezeHandling::fhIGNORE_FREEZE, - AuthHandling::ahIGNORE_AUTH, - j)) - { - JLOG(j.fatal()) << "Invariant failed: Loan Broker cover available " - "is less than pseudo-account asset balance"; - return false; - } - } - return true; -} - -//------------------------------------------------------------------------------ - -void -ValidLoan::visitEntry( - bool isDelete, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - if (after && after->getType() == ltLOAN) - { - loans_.emplace_back(before, after); - } -} - -bool -ValidLoan::finalize( - STTx const& tx, - TER const, - XRPAmount const, - ReadView const& view, - beast::Journal const& j) -{ - // Loans will not exist on ledger if the Lending Protocol amendment - // is not enabled, so there's no need to check it. - - for (auto const& [before, after] : loans_) - { - // https://github.com/Tapanito/XRPL-Standards/blob/xls-66-lending-protocol/XLS-0066d-lending-protocol/README.md#3223-invariants - // If `Loan.PaymentRemaining = 0` then the loan MUST be fully paid off - if (after->at(sfPaymentRemaining) == 0 && - (after->at(sfTotalValueOutstanding) != beast::zero || - after->at(sfPrincipalOutstanding) != beast::zero || - after->at(sfManagementFeeOutstanding) != beast::zero)) - { - JLOG(j.fatal()) << "Invariant failed: Loan with zero payments " - "remaining has not been paid off"; - return false; - } - // If `Loan.PaymentRemaining != 0` then the loan MUST NOT be fully paid - // off - if (after->at(sfPaymentRemaining) != 0 && - after->at(sfTotalValueOutstanding) == beast::zero && - after->at(sfPrincipalOutstanding) == beast::zero && - after->at(sfManagementFeeOutstanding) == beast::zero) - { - JLOG(j.fatal()) << "Invariant failed: Loan with zero payments " - "remaining has not been paid off"; - return false; - } - if (before && (before->isFlag(lsfLoanOverpayment) != after->isFlag(lsfLoanOverpayment))) - { - JLOG(j.fatal()) << "Invariant failed: Loan Overpayment flag changed"; - return false; - } - // Must not be negative - STNumber - for (auto const field : - {&sfLoanServiceFee, - &sfLatePaymentFee, - &sfClosePaymentFee, - &sfPrincipalOutstanding, - &sfTotalValueOutstanding, - &sfManagementFeeOutstanding}) - { - if (after->at(*field) < 0) - { - JLOG(j.fatal()) << "Invariant failed: " << field->getName() << " is negative "; - return false; - } - } - // Must be positive - STNumber - for (auto const field : { - &sfPeriodicPayment, - }) - { - if (after->at(*field) <= 0) - { - JLOG(j.fatal()) << "Invariant failed: " << field->getName() - << " is zero or negative "; - return false; - } - } - } - return true; -} - -ValidVault::Vault -ValidVault::Vault::make(SLE const& from) -{ - XRPL_ASSERT(from.getType() == ltVAULT, "ValidVault::Vault::make : from Vault object"); - - ValidVault::Vault self; - self.key = from.key(); - self.asset = from.at(sfAsset); - self.pseudoId = from.getAccountID(sfAccount); - self.owner = from.at(sfOwner); - self.shareMPTID = from.getFieldH192(sfShareMPTID); - self.assetsTotal = from.at(sfAssetsTotal); - self.assetsAvailable = from.at(sfAssetsAvailable); - self.assetsMaximum = from.at(sfAssetsMaximum); - self.lossUnrealized = from.at(sfLossUnrealized); - return self; -} - -ValidVault::Shares -ValidVault::Shares::make(SLE const& from) -{ - XRPL_ASSERT( - from.getType() == ltMPTOKEN_ISSUANCE, - "ValidVault::Shares::make : from MPTokenIssuance object"); - - ValidVault::Shares self; - self.share = MPTIssue(makeMptID(from.getFieldU32(sfSequence), from.getAccountID(sfIssuer))); - self.sharesTotal = from.at(sfOutstandingAmount); - self.sharesMaximum = from[~sfMaximumAmount].value_or(maxMPTokenAmount); - return self; -} - -void -ValidVault::visitEntry( - bool isDelete, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - // If `before` is empty, this means an object is being created, in which - // case `isDelete` must be false. Otherwise `before` and `after` are set and - // `isDelete` indicates whether an object is being deleted or modified. - XRPL_ASSERT( - after != nullptr && (before != nullptr || !isDelete), - "xrpl::ValidVault::visitEntry : some object is available"); - - // Number balanceDelta will capture the difference (delta) between "before" - // state (zero if created) and "after" state (zero if destroyed), so the - // invariants can validate that the change in account balances matches the - // change in vault balances, stored to deltas_ at the end of this function. - Number balanceDelta{}; - - std::int8_t sign = 0; - if (before) - { - switch (before->getType()) - { - case ltVAULT: - beforeVault_.push_back(Vault::make(*before)); - break; - case ltMPTOKEN_ISSUANCE: - // At this moment we have no way of telling if this object holds - // vault shares or something else. Save it for finalize. - beforeMPTs_.push_back(Shares::make(*before)); - balanceDelta = static_cast(before->getFieldU64(sfOutstandingAmount)); - sign = 1; - break; - case ltMPTOKEN: - balanceDelta = static_cast(before->getFieldU64(sfMPTAmount)); - sign = -1; - break; - case ltACCOUNT_ROOT: - case ltRIPPLE_STATE: - balanceDelta = before->getFieldAmount(sfBalance); - sign = -1; - break; - default:; - } - } - - if (!isDelete && after) - { - switch (after->getType()) - { - case ltVAULT: - afterVault_.push_back(Vault::make(*after)); - break; - case ltMPTOKEN_ISSUANCE: - // At this moment we have no way of telling if this object holds - // vault shares or something else. Save it for finalize. - afterMPTs_.push_back(Shares::make(*after)); - balanceDelta -= - Number(static_cast(after->getFieldU64(sfOutstandingAmount))); - sign = 1; - break; - case ltMPTOKEN: - balanceDelta -= Number(static_cast(after->getFieldU64(sfMPTAmount))); - sign = -1; - break; - case ltACCOUNT_ROOT: - case ltRIPPLE_STATE: - balanceDelta -= Number(after->getFieldAmount(sfBalance)); - sign = -1; - break; - default:; - } - } - - uint256 const key = (before ? before->key() : after->key()); - // Append to deltas if sign is non-zero, i.e. an object of an interesting - // type has been updated. A transaction may update an object even when - // its balance has not changed, e.g. transaction fee equals the amount - // transferred to the account. We intentionally do not compare balanceDelta - // against zero, to avoid missing such updates. - if (sign != 0) - deltas_[key] = balanceDelta * sign; -} - -bool -ValidVault::finalize( - STTx const& tx, - TER const ret, - XRPAmount const fee, - ReadView const& view, - beast::Journal const& j) -{ - bool const enforce = view.rules().enabled(featureSingleAssetVault); - - if (!isTesSuccess(ret)) - return true; // Do not perform checks - - if (afterVault_.empty() && beforeVault_.empty()) - { - if (hasPrivilege(tx, mustModifyVault)) - { - JLOG(j.fatal()) << // - "Invariant failed: vault operation succeeded without modifying " - "a vault"; - XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault noop invariant"); - return !enforce; - } - - return true; // Not a vault operation - } - else if (!(hasPrivilege(tx, mustModifyVault) || hasPrivilege(tx, mayModifyVault))) - { - JLOG(j.fatal()) << // - "Invariant failed: vault updated by a wrong transaction type"; - XRPL_ASSERT( - enforce, - "xrpl::ValidVault::finalize : illegal vault transaction " - "invariant"); - return !enforce; // Also not a vault operation - } - - if (beforeVault_.size() > 1 || afterVault_.size() > 1) - { - JLOG(j.fatal()) << // - "Invariant failed: vault operation updated more than single vault"; - XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : single vault invariant"); - return !enforce; // That's all we can do here - } - - auto const txnType = tx.getTxnType(); - - // We do special handling for ttVAULT_DELETE first, because it's the only - // vault-modifying transaction without an "after" state of the vault - if (afterVault_.empty()) - { - if (txnType != ttVAULT_DELETE) - { - JLOG(j.fatal()) << // - "Invariant failed: vault deleted by a wrong transaction type"; - XRPL_ASSERT( - enforce, - "xrpl::ValidVault::finalize : illegal vault deletion " - "invariant"); - return !enforce; // That's all we can do here - } - - // Note, if afterVault_ is empty then we know that beforeVault_ is not - // empty, as enforced at the top of this function - auto const& beforeVault = beforeVault_[0]; - - // At this moment we only know a vault is being deleted and there - // might be some MPTokenIssuance objects which are deleted in the - // same transaction. Find the one matching this vault. - auto const deletedShares = [&]() -> std::optional { - for (auto const& e : beforeMPTs_) - { - if (e.share.getMptID() == beforeVault.shareMPTID) - return std::move(e); - } - return std::nullopt; - }(); - - if (!deletedShares) - { - JLOG(j.fatal()) << "Invariant failed: deleted vault must also " - "delete shares"; - XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : shares deletion invariant"); - return !enforce; // That's all we can do here - } - - bool result = true; - if (deletedShares->sharesTotal != 0) - { - JLOG(j.fatal()) << "Invariant failed: deleted vault must have no " - "shares outstanding"; - result = false; - } - if (beforeVault.assetsTotal != zero) - { - JLOG(j.fatal()) << "Invariant failed: deleted vault must have no " - "assets outstanding"; - result = false; - } - if (beforeVault.assetsAvailable != zero) - { - JLOG(j.fatal()) << "Invariant failed: deleted vault must have no " - "assets available"; - result = false; - } - - return result; - } - else if (txnType == ttVAULT_DELETE) - { - JLOG(j.fatal()) << "Invariant failed: vault deletion succeeded without " - "deleting a vault"; - XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault deletion invariant"); - return !enforce; // That's all we can do here - } - - // Note, `afterVault_.empty()` is handled above - auto const& afterVault = afterVault_[0]; - XRPL_ASSERT( - beforeVault_.empty() || beforeVault_[0].key == afterVault.key, - "xrpl::ValidVault::finalize : single vault operation"); - - auto const updatedShares = [&]() -> std::optional { - // At this moment we only know that a vault is being updated and there - // might be some MPTokenIssuance objects which are also updated in the - // same transaction. Find the one matching the shares to this vault. - // Note, we expect updatedMPTs collection to be extremely small. For - // such collections linear search is faster than lookup. - for (auto const& e : afterMPTs_) - { - if (e.share.getMptID() == afterVault.shareMPTID) - return e; - } - - auto const sleShares = view.read(keylet::mptIssuance(afterVault.shareMPTID)); - - return sleShares ? std::optional(Shares::make(*sleShares)) : std::nullopt; - }(); - - bool result = true; - - // Universal transaction checks - if (!beforeVault_.empty()) - { - auto const& beforeVault = beforeVault_[0]; - if (afterVault.asset != beforeVault.asset || afterVault.pseudoId != beforeVault.pseudoId || - afterVault.shareMPTID != beforeVault.shareMPTID) - { - JLOG(j.fatal()) << "Invariant failed: violation of vault immutable data"; - result = false; - } - } - - if (!updatedShares) - { - JLOG(j.fatal()) << "Invariant failed: updated vault must have shares"; - XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault has shares invariant"); - return !enforce; // That's all we can do here - } - - if (updatedShares->sharesTotal == 0) - { - if (afterVault.assetsTotal != zero) - { - JLOG(j.fatal()) << "Invariant failed: updated zero sized " - "vault must have no assets outstanding"; - result = false; - } - if (afterVault.assetsAvailable != zero) - { - JLOG(j.fatal()) << "Invariant failed: updated zero sized " - "vault must have no assets available"; - result = false; - } - } - else if (updatedShares->sharesTotal > updatedShares->sharesMaximum) - { - JLOG(j.fatal()) // - << "Invariant failed: updated shares must not exceed maximum " - << updatedShares->sharesMaximum; - result = false; - } - - if (afterVault.assetsAvailable < zero) - { - JLOG(j.fatal()) << "Invariant failed: assets available must be positive"; - result = false; - } - - if (afterVault.assetsAvailable > afterVault.assetsTotal) - { - JLOG(j.fatal()) << "Invariant failed: assets available must " - "not be greater than assets outstanding"; - result = false; - } - else if (afterVault.lossUnrealized > afterVault.assetsTotal - afterVault.assetsAvailable) - { - JLOG(j.fatal()) // - << "Invariant failed: loss unrealized must not exceed " - "the difference between assets outstanding and available"; - result = false; - } - - if (afterVault.assetsTotal < zero) - { - JLOG(j.fatal()) << "Invariant failed: assets outstanding must be positive"; - result = false; - } - - if (afterVault.assetsMaximum < zero) - { - JLOG(j.fatal()) << "Invariant failed: assets maximum must be positive"; - result = false; - } - - // Thanks to this check we can simply do `assert(!beforeVault_.empty()` when - // enforcing invariants on transaction types other than ttVAULT_CREATE - if (beforeVault_.empty() && txnType != ttVAULT_CREATE) - { - JLOG(j.fatal()) << // - "Invariant failed: vault created by a wrong transaction type"; - XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault creation invariant"); - return !enforce; // That's all we can do here - } - - if (!beforeVault_.empty() && afterVault.lossUnrealized != beforeVault_[0].lossUnrealized && - txnType != ttLOAN_MANAGE && txnType != ttLOAN_PAY) - { - JLOG(j.fatal()) << // - "Invariant failed: vault transaction must not change loss " - "unrealized"; - result = false; - } - - auto const beforeShares = [&]() -> std::optional { - if (beforeVault_.empty()) - return std::nullopt; - auto const& beforeVault = beforeVault_[0]; - - for (auto const& e : beforeMPTs_) - { - if (e.share.getMptID() == beforeVault.shareMPTID) - return std::move(e); - } - return std::nullopt; - }(); - - if (!beforeShares && - (tx.getTxnType() == ttVAULT_DEPOSIT || // - tx.getTxnType() == ttVAULT_WITHDRAW || // - tx.getTxnType() == ttVAULT_CLAWBACK)) - { - JLOG(j.fatal()) << "Invariant failed: vault operation succeeded " - "without updating shares"; - XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : shares noop invariant"); - return !enforce; // That's all we can do here - } - - auto const& vaultAsset = afterVault.asset; - auto const deltaAssets = [&](AccountID const& id) -> std::optional { - auto const get = // - [&](auto const& it, std::int8_t sign = 1) -> std::optional { - if (it == deltas_.end()) - return std::nullopt; - - return it->second * sign; - }; - - return std::visit( - [&](TIss const& issue) { - if constexpr (std::is_same_v) - { - if (isXRP(issue)) - return get(deltas_.find(keylet::account(id).key)); - return get( - deltas_.find(keylet::line(id, issue).key), id > issue.getIssuer() ? -1 : 1); - } - else if constexpr (std::is_same_v) - { - return get(deltas_.find(keylet::mptoken(issue.getMptID(), id).key)); - } - }, - vaultAsset.value()); - }; - auto const deltaAssetsTxAccount = [&]() -> std::optional { - auto ret = deltaAssets(tx[sfAccount]); - // Nothing returned or not XRP transaction - if (!ret.has_value() || !vaultAsset.native()) - return ret; - - // Delegated transaction; no need to compensate for fees - if (auto const delegate = tx[~sfDelegate]; - delegate.has_value() && *delegate != tx[sfAccount]) - return ret; - - *ret += fee.drops(); - if (*ret == zero) - return std::nullopt; - - return ret; - }; - auto const deltaShares = [&](AccountID const& id) -> std::optional { - auto const it = [&]() { - if (id == afterVault.pseudoId) - return deltas_.find(keylet::mptIssuance(afterVault.shareMPTID).key); - return deltas_.find(keylet::mptoken(afterVault.shareMPTID, id).key); - }(); - - return it != deltas_.end() ? std::optional(it->second) : std::nullopt; - }; - - auto const vaultHoldsNoAssets = [&](Vault const& vault) { - return vault.assetsAvailable == 0 && vault.assetsTotal == 0; - }; - - // Technically this does not need to be a lambda, but it's more - // convenient thanks to early "return false"; the not-so-nice - // alternatives are several layers of nested if/else or more complex - // (i.e. brittle) if statements. - result &= [&]() { - switch (txnType) - { - case ttVAULT_CREATE: { - bool result = true; - - if (!beforeVault_.empty()) - { - JLOG(j.fatal()) // - << "Invariant failed: create operation must not have " - "updated a vault"; - result = false; - } - - if (afterVault.assetsAvailable != zero || afterVault.assetsTotal != zero || - afterVault.lossUnrealized != zero || updatedShares->sharesTotal != 0) - { - JLOG(j.fatal()) // - << "Invariant failed: created vault must be empty"; - result = false; - } - - if (afterVault.pseudoId != updatedShares->share.getIssuer()) - { - JLOG(j.fatal()) // - << "Invariant failed: shares issuer and vault " - "pseudo-account must be the same"; - result = false; - } - - auto const sleSharesIssuer = - view.read(keylet::account(updatedShares->share.getIssuer())); - if (!sleSharesIssuer) - { - JLOG(j.fatal()) // - << "Invariant failed: shares issuer must exist"; - return false; - } - - if (!isPseudoAccount(sleSharesIssuer)) - { - JLOG(j.fatal()) // - << "Invariant failed: shares issuer must be a " - "pseudo-account"; - result = false; - } - - if (auto const vaultId = (*sleSharesIssuer)[~sfVaultID]; - !vaultId || *vaultId != afterVault.key) - { - JLOG(j.fatal()) // - << "Invariant failed: shares issuer pseudo-account " - "must point back to the vault"; - result = false; - } - - return result; - } - case ttVAULT_SET: { - bool result = true; - - XRPL_ASSERT( - !beforeVault_.empty(), "xrpl::ValidVault::finalize : set updated a vault"); - auto const& beforeVault = beforeVault_[0]; - - auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId); - if (vaultDeltaAssets) - { - JLOG(j.fatal()) << // - "Invariant failed: set must not change vault balance"; - result = false; - } - - if (beforeVault.assetsTotal != afterVault.assetsTotal) - { - JLOG(j.fatal()) << // - "Invariant failed: set must not change assets " - "outstanding"; - result = false; - } - - if (afterVault.assetsMaximum > zero && - afterVault.assetsTotal > afterVault.assetsMaximum) - { - JLOG(j.fatal()) << // - "Invariant failed: set assets outstanding must not " - "exceed assets maximum"; - result = false; - } - - if (beforeVault.assetsAvailable != afterVault.assetsAvailable) - { - JLOG(j.fatal()) << // - "Invariant failed: set must not change assets " - "available"; - result = false; - } - - if (beforeShares && updatedShares && - beforeShares->sharesTotal != updatedShares->sharesTotal) - { - JLOG(j.fatal()) << // - "Invariant failed: set must not change shares " - "outstanding"; - result = false; - } - - return result; - } - case ttVAULT_DEPOSIT: { - bool result = true; - - XRPL_ASSERT( - !beforeVault_.empty(), "xrpl::ValidVault::finalize : deposit updated a vault"); - auto const& beforeVault = beforeVault_[0]; - - auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId); - - if (!vaultDeltaAssets) - { - JLOG(j.fatal()) << // - "Invariant failed: deposit must change vault balance"; - return false; // That's all we can do - } - - if (*vaultDeltaAssets > tx[sfAmount]) - { - JLOG(j.fatal()) << // - "Invariant failed: deposit must not change vault " - "balance by more than deposited amount"; - result = false; - } - - if (*vaultDeltaAssets <= zero) - { - JLOG(j.fatal()) << // - "Invariant failed: deposit must increase vault balance"; - result = false; - } - - // Any payments (including deposits) made by the issuer - // do not change their balance, but create funds instead. - bool const issuerDeposit = [&]() -> bool { - if (vaultAsset.native()) - return false; - return tx[sfAccount] == vaultAsset.getIssuer(); - }(); - - if (!issuerDeposit) - { - auto const accountDeltaAssets = deltaAssetsTxAccount(); - if (!accountDeltaAssets) - { - JLOG(j.fatal()) << // - "Invariant failed: deposit must change depositor " - "balance"; - return false; - } - - if (*accountDeltaAssets >= zero) - { - JLOG(j.fatal()) << // - "Invariant failed: deposit must decrease depositor " - "balance"; - result = false; - } - - if (*accountDeltaAssets * -1 != *vaultDeltaAssets) - { - JLOG(j.fatal()) << // - "Invariant failed: deposit must change vault and " - "depositor balance by equal amount"; - result = false; - } - } - - if (afterVault.assetsMaximum > zero && - afterVault.assetsTotal > afterVault.assetsMaximum) - { - JLOG(j.fatal()) << // - "Invariant failed: deposit assets outstanding must not " - "exceed assets maximum"; - result = false; - } - - auto const accountDeltaShares = deltaShares(tx[sfAccount]); - if (!accountDeltaShares) - { - JLOG(j.fatal()) << // - "Invariant failed: deposit must change depositor " - "shares"; - return false; // That's all we can do - } - - if (*accountDeltaShares <= zero) - { - JLOG(j.fatal()) << // - "Invariant failed: deposit must increase depositor " - "shares"; - result = false; - } - - auto const vaultDeltaShares = deltaShares(afterVault.pseudoId); - if (!vaultDeltaShares || *vaultDeltaShares == zero) - { - JLOG(j.fatal()) << // - "Invariant failed: deposit must change vault shares"; - return false; // That's all we can do - } - - if (*vaultDeltaShares * -1 != *accountDeltaShares) - { - JLOG(j.fatal()) << // - "Invariant failed: deposit must change depositor and " - "vault shares by equal amount"; - result = false; - } - - if (beforeVault.assetsTotal + *vaultDeltaAssets != afterVault.assetsTotal) - { - JLOG(j.fatal()) << "Invariant failed: deposit and assets " - "outstanding must add up"; - result = false; - } - if (beforeVault.assetsAvailable + *vaultDeltaAssets != afterVault.assetsAvailable) - { - JLOG(j.fatal()) << "Invariant failed: deposit and assets " - "available must add up"; - result = false; - } - - return result; - } - case ttVAULT_WITHDRAW: { - bool result = true; - - XRPL_ASSERT( - !beforeVault_.empty(), - "xrpl::ValidVault::finalize : withdrawal updated a " - "vault"); - auto const& beforeVault = beforeVault_[0]; - - auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId); - - if (!vaultDeltaAssets) - { - JLOG(j.fatal()) << "Invariant failed: withdrawal must " - "change vault balance"; - return false; // That's all we can do - } - - if (*vaultDeltaAssets >= zero) - { - JLOG(j.fatal()) << "Invariant failed: withdrawal must " - "decrease vault balance"; - result = false; - } - - // Any payments (including withdrawal) going to the issuer - // do not change their balance, but destroy funds instead. - bool const issuerWithdrawal = [&]() -> bool { - if (vaultAsset.native()) - return false; - auto const destination = tx[~sfDestination].value_or(tx[sfAccount]); - return destination == vaultAsset.getIssuer(); - }(); - - if (!issuerWithdrawal) - { - auto const accountDeltaAssets = deltaAssetsTxAccount(); - auto const otherAccountDelta = [&]() -> std::optional { - if (auto const destination = tx[~sfDestination]; - destination && *destination != tx[sfAccount]) - return deltaAssets(*destination); - return std::nullopt; - }(); - - if (accountDeltaAssets.has_value() == otherAccountDelta.has_value()) - { - JLOG(j.fatal()) << // - "Invariant failed: withdrawal must change one " - "destination balance"; - return false; - } - - auto const destinationDelta = // - accountDeltaAssets ? *accountDeltaAssets : *otherAccountDelta; - - if (destinationDelta <= zero) - { - JLOG(j.fatal()) << // - "Invariant failed: withdrawal must increase " - "destination balance"; - result = false; - } - - if (*vaultDeltaAssets * -1 != destinationDelta) - { - JLOG(j.fatal()) << // - "Invariant failed: withdrawal must change vault " - "and destination balance by equal amount"; - result = false; - } - } - - auto const accountDeltaShares = deltaShares(tx[sfAccount]); - if (!accountDeltaShares) - { - JLOG(j.fatal()) << // - "Invariant failed: withdrawal must change depositor " - "shares"; - return false; - } - - if (*accountDeltaShares >= zero) - { - JLOG(j.fatal()) << // - "Invariant failed: withdrawal must decrease depositor " - "shares"; - result = false; - } - - auto const vaultDeltaShares = deltaShares(afterVault.pseudoId); - if (!vaultDeltaShares || *vaultDeltaShares == zero) - { - JLOG(j.fatal()) << // - "Invariant failed: withdrawal must change vault shares"; - return false; // That's all we can do - } - - if (*vaultDeltaShares * -1 != *accountDeltaShares) - { - JLOG(j.fatal()) << // - "Invariant failed: withdrawal must change depositor " - "and vault shares by equal amount"; - result = false; - } - - // Note, vaultBalance is negative (see check above) - if (beforeVault.assetsTotal + *vaultDeltaAssets != afterVault.assetsTotal) - { - JLOG(j.fatal()) << "Invariant failed: withdrawal and " - "assets outstanding must add up"; - result = false; - } - - if (beforeVault.assetsAvailable + *vaultDeltaAssets != afterVault.assetsAvailable) - { - JLOG(j.fatal()) << "Invariant failed: withdrawal and " - "assets available must add up"; - result = false; - } - - return result; - } - case ttVAULT_CLAWBACK: { - bool result = true; - - XRPL_ASSERT( - !beforeVault_.empty(), "xrpl::ValidVault::finalize : clawback updated a vault"); - auto const& beforeVault = beforeVault_[0]; - - if (vaultAsset.native() || vaultAsset.getIssuer() != tx[sfAccount]) - { - // The owner can use clawback to force-burn shares when the - // vault is empty but there are outstanding shares - if (!(beforeShares && beforeShares->sharesTotal > 0 && - vaultHoldsNoAssets(beforeVault) && beforeVault.owner == tx[sfAccount])) - { - JLOG(j.fatal()) << // - "Invariant failed: clawback may only be performed " - "by the asset issuer, or by the vault owner of an " - "empty vault"; - return false; // That's all we can do - } - } - - auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId); - if (vaultDeltaAssets) - { - if (*vaultDeltaAssets >= zero) - { - JLOG(j.fatal()) << // - "Invariant failed: clawback must decrease vault " - "balance"; - result = false; - } - - if (beforeVault.assetsTotal + *vaultDeltaAssets != afterVault.assetsTotal) - { - JLOG(j.fatal()) << // - "Invariant failed: clawback and assets outstanding " - "must add up"; - result = false; - } - - if (beforeVault.assetsAvailable + *vaultDeltaAssets != - afterVault.assetsAvailable) - { - JLOG(j.fatal()) << // - "Invariant failed: clawback and assets available " - "must add up"; - result = false; - } - } - else if (!vaultHoldsNoAssets(beforeVault)) - { - JLOG(j.fatal()) << // - "Invariant failed: clawback must change vault balance"; - return false; // That's all we can do - } - - auto const accountDeltaShares = deltaShares(tx[sfHolder]); - if (!accountDeltaShares) - { - JLOG(j.fatal()) << // - "Invariant failed: clawback must change holder shares"; - return false; // That's all we can do - } - - if (*accountDeltaShares >= zero) - { - JLOG(j.fatal()) << // - "Invariant failed: clawback must decrease holder " - "shares"; - result = false; - } - - auto const vaultDeltaShares = deltaShares(afterVault.pseudoId); - if (!vaultDeltaShares || *vaultDeltaShares == zero) - { - JLOG(j.fatal()) << // - "Invariant failed: clawback must change vault shares"; - return false; // That's all we can do - } - - if (*vaultDeltaShares * -1 != *accountDeltaShares) - { - JLOG(j.fatal()) << // - "Invariant failed: clawback must change holder and " - "vault shares by equal amount"; - result = false; - } - - return result; - } - - case ttLOAN_SET: - case ttLOAN_MANAGE: - case ttLOAN_PAY: { - // TBD - return true; - } - - default: - // LCOV_EXCL_START - UNREACHABLE("xrpl::ValidVault::finalize : unknown transaction type"); - return false; - // LCOV_EXCL_STOP - } - }(); - - if (!result) - { - // The comment at the top of this file starting with "assert(enforce)" - // explains this assert. - XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault invariants"); - return !enforce; - } - - return true; -} - -} // namespace xrpl diff --git a/src/libxrpl/tx/invariants/AMMInvariant.cpp b/src/libxrpl/tx/invariants/AMMInvariant.cpp new file mode 100644 index 0000000000..d98c0a6f50 --- /dev/null +++ b/src/libxrpl/tx/invariants/AMMInvariant.cpp @@ -0,0 +1,305 @@ +#include +// +#include +#include +#include +#include +#include + +namespace xrpl { + +void +ValidAMM::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (isDelete) + return; + + if (after) + { + auto const type = after->getType(); + // AMM object changed + if (type == ltAMM) + { + ammAccount_ = after->getAccountID(sfAccount); + lptAMMBalanceAfter_ = after->getFieldAmount(sfLPTokenBalance); + } + // AMM pool changed + else if ( + (type == ltRIPPLE_STATE && after->getFlags() & lsfAMMNode) || + (type == ltACCOUNT_ROOT && after->isFieldPresent(sfAMMID))) + { + ammPoolChanged_ = true; + } + } + + if (before) + { + // AMM object changed + if (before->getType() == ltAMM) + { + lptAMMBalanceBefore_ = before->getFieldAmount(sfLPTokenBalance); + } + } +} + +static bool +validBalances( + STAmount const& amount, + STAmount const& amount2, + STAmount const& lptAMMBalance, + ValidAMM::ZeroAllowed zeroAllowed) +{ + bool const positive = + amount > beast::zero && amount2 > beast::zero && lptAMMBalance > beast::zero; + if (zeroAllowed == ValidAMM::ZeroAllowed::Yes) + return positive || + (amount == beast::zero && amount2 == beast::zero && lptAMMBalance == beast::zero); + return positive; +} + +bool +ValidAMM::finalizeVote(bool enforce, beast::Journal const& j) const +{ + if (lptAMMBalanceAfter_ != lptAMMBalanceBefore_ || ammPoolChanged_) + { + // LPTokens and the pool can not change on vote + // LCOV_EXCL_START + JLOG(j.error()) << "AMMVote invariant failed: " << lptAMMBalanceBefore_.value_or(STAmount{}) + << " " << lptAMMBalanceAfter_.value_or(STAmount{}) << " " + << ammPoolChanged_; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + + return true; +} + +bool +ValidAMM::finalizeBid(bool enforce, beast::Journal const& j) const +{ + if (ammPoolChanged_) + { + // The pool can not change on bid + // LCOV_EXCL_START + JLOG(j.error()) << "AMMBid invariant failed: pool changed"; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + // LPTokens are burnt, therefore there should be fewer LPTokens + else if ( + lptAMMBalanceBefore_ && lptAMMBalanceAfter_ && + (*lptAMMBalanceAfter_ > *lptAMMBalanceBefore_ || *lptAMMBalanceAfter_ <= beast::zero)) + { + // LCOV_EXCL_START + JLOG(j.error()) << "AMMBid invariant failed: " << *lptAMMBalanceBefore_ << " " + << *lptAMMBalanceAfter_; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + + return true; +} + +bool +ValidAMM::finalizeCreate( + STTx const& tx, + ReadView const& view, + bool enforce, + beast::Journal const& j) const +{ + if (!ammAccount_) + { + // LCOV_EXCL_START + JLOG(j.error()) << "AMMCreate invariant failed: AMM object is not created"; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + else + { + auto const [amount, amount2] = ammPoolHolds( + view, + *ammAccount_, + tx[sfAmount].get(), + tx[sfAmount2].get(), + fhIGNORE_FREEZE, + j); + // Create invariant: + // sqrt(amount * amount2) == LPTokens + // all balances are greater than zero + if (!validBalances(amount, amount2, *lptAMMBalanceAfter_, ZeroAllowed::No) || + ammLPTokens(amount, amount2, lptAMMBalanceAfter_->issue()) != *lptAMMBalanceAfter_) + { + JLOG(j.error()) << "AMMCreate invariant failed: " << amount << " " << amount2 << " " + << *lptAMMBalanceAfter_; + if (enforce) + return false; + } + } + + return true; +} + +bool +ValidAMM::finalizeDelete(bool enforce, TER res, beast::Journal const& j) const +{ + if (ammAccount_) + { + // LCOV_EXCL_START + std::string const msg = (res == tesSUCCESS) ? "AMM object is not deleted on tesSUCCESS" + : "AMM object is changed on tecINCOMPLETE"; + JLOG(j.error()) << "AMMDelete invariant failed: " << msg; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + + return true; +} + +bool +ValidAMM::finalizeDEX(bool enforce, beast::Journal const& j) const +{ + if (ammAccount_) + { + // LCOV_EXCL_START + JLOG(j.error()) << "AMM swap invariant failed: AMM object changed"; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + + return true; +} + +bool +ValidAMM::generalInvariant( + xrpl::STTx const& tx, + xrpl::ReadView const& view, + ZeroAllowed zeroAllowed, + beast::Journal const& j) const +{ + auto const [amount, amount2] = ammPoolHolds( + view, + *ammAccount_, + tx[sfAsset].get(), + tx[sfAsset2].get(), + fhIGNORE_FREEZE, + j); + // Deposit and Withdrawal invariant: + // sqrt(amount * amount2) >= LPTokens + // all balances are greater than zero + // unless on last withdrawal + auto const poolProductMean = root2(amount * amount2); + bool const nonNegativeBalances = + validBalances(amount, amount2, *lptAMMBalanceAfter_, zeroAllowed); + bool const strongInvariantCheck = poolProductMean >= *lptAMMBalanceAfter_; + // Allow for a small relative error if strongInvariantCheck fails + auto weakInvariantCheck = [&]() { + return *lptAMMBalanceAfter_ != beast::zero && + withinRelativeDistance(poolProductMean, Number{*lptAMMBalanceAfter_}, Number{1, -11}); + }; + if (!nonNegativeBalances || (!strongInvariantCheck && !weakInvariantCheck())) + { + JLOG(j.error()) << "AMM " << tx.getTxnType() + << " invariant failed: " << tx.getHash(HashPrefix::transactionID) << " " + << ammPoolChanged_ << " " << amount << " " << amount2 << " " + << poolProductMean << " " << lptAMMBalanceAfter_->getText() << " " + << ((*lptAMMBalanceAfter_ == beast::zero) + ? Number{1} + : ((*lptAMMBalanceAfter_ - poolProductMean) / poolProductMean)); + return false; + } + + return true; +} + +bool +ValidAMM::finalizeDeposit( + xrpl::STTx const& tx, + xrpl::ReadView const& view, + bool enforce, + beast::Journal const& j) const +{ + if (!ammAccount_) + { + // LCOV_EXCL_START + JLOG(j.error()) << "AMMDeposit invariant failed: AMM object is deleted"; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + else if (!generalInvariant(tx, view, ZeroAllowed::No, j) && enforce) + return false; + + return true; +} + +bool +ValidAMM::finalizeWithdraw( + xrpl::STTx const& tx, + xrpl::ReadView const& view, + bool enforce, + beast::Journal const& j) const +{ + if (!ammAccount_) + { + // Last Withdraw or Clawback deleted AMM + } + else if (!generalInvariant(tx, view, ZeroAllowed::Yes, j)) + { + if (enforce) + return false; + } + + return true; +} + +bool +ValidAMM::finalize( + STTx const& tx, + TER const result, + XRPAmount const, + ReadView const& view, + beast::Journal const& j) +{ + // Delete may return tecINCOMPLETE if there are too many + // trustlines to delete. + if (result != tesSUCCESS && result != tecINCOMPLETE) + return true; + + bool const enforce = view.rules().enabled(fixAMMv1_3); + + switch (tx.getTxnType()) + { + case ttAMM_CREATE: + return finalizeCreate(tx, view, enforce, j); + case ttAMM_DEPOSIT: + return finalizeDeposit(tx, view, enforce, j); + case ttAMM_CLAWBACK: + case ttAMM_WITHDRAW: + return finalizeWithdraw(tx, view, enforce, j); + case ttAMM_BID: + return finalizeBid(enforce, j); + case ttAMM_VOTE: + return finalizeVote(enforce, j); + case ttAMM_DELETE: + return finalizeDelete(enforce, result, j); + case ttCHECK_CASH: + case ttOFFER_CREATE: + case ttPAYMENT: + return finalizeDEX(enforce, j); + default: + break; + } + + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/invariants/FreezeInvariant.cpp b/src/libxrpl/tx/invariants/FreezeInvariant.cpp new file mode 100644 index 0000000000..858c4cdcb8 --- /dev/null +++ b/src/libxrpl/tx/invariants/FreezeInvariant.cpp @@ -0,0 +1,278 @@ +#include +// +#include +#include +#include +#include +#include + +namespace xrpl { + +void +TransfersNotFrozen::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + /* + * A trust line freeze state alone doesn't determine if a transfer is + * frozen. The transfer must be examined "end-to-end" because both sides of + * the transfer may have different freeze states and freeze impact depends + * on the transfer direction. This is why first we need to track the + * transfers using IssuerChanges senders/receivers. + * + * Only in validateIssuerChanges, after we collected all changes can we + * determine if the transfer is valid. + */ + if (!isValidEntry(before, after)) + { + return; + } + + auto const balanceChange = calculateBalanceChange(before, after, isDelete); + if (balanceChange.signum() == 0) + { + return; + } + + recordBalanceChanges(after, balanceChange); +} + +bool +TransfersNotFrozen::finalize( + STTx const& tx, + TER const ter, + XRPAmount const fee, + ReadView const& view, + beast::Journal const& j) +{ + /* + * We check this invariant regardless of deep freeze amendment status, + * allowing for detection and logging of potential issues even when the + * amendment is disabled. + * + * If an exploit that allows moving frozen assets is discovered, + * we can alert operators who monitor fatal messages and trigger assert in + * debug builds for an early warning. + * + * In an unlikely event that an exploit is found, this early detection + * enables encouraging the UNL to expedite deep freeze amendment activation + * or deploy hotfixes via new amendments. In case of a new amendment, we'd + * only have to change this line setting 'enforce' variable. + * enforce = view.rules().enabled(featureDeepFreeze) || + * view.rules().enabled(fixFreezeExploit); + */ + [[maybe_unused]] bool const enforce = view.rules().enabled(featureDeepFreeze); + + for (auto const& [issue, changes] : balanceChanges_) + { + auto const issuerSle = findIssuer(issue.account, view); + // It should be impossible for the issuer to not be found, but check + // 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, + "xrpl::TransfersNotFrozen::finalize : enforce " + "invariant."); + if (enforce) + { + return false; + } + continue; + } + + if (!validateIssuerChanges(issuerSle, changes, tx, j, enforce)) + { + return false; + } + } + + return true; +} + +bool +TransfersNotFrozen::isValidEntry( + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + // `after` can never be null, even if the trust line is deleted. + XRPL_ASSERT(after, "xrpl::TransfersNotFrozen::isValidEntry : valid after."); + if (!after) + { + return false; + } + + if (after->getType() == ltACCOUNT_ROOT) + { + possibleIssuers_.emplace(after->at(sfAccount), after); + return false; + } + + /* While LedgerEntryTypesMatch invariant also checks types, all invariants + * are processed regardless of previous failures. + * + * This type check is still necessary here because it prevents potential + * issues in subsequent processing. + */ + return after->getType() == ltRIPPLE_STATE && (!before || before->getType() == ltRIPPLE_STATE); +} + +STAmount +TransfersNotFrozen::calculateBalanceChange( + std::shared_ptr const& before, + std::shared_ptr const& after, + bool isDelete) +{ + auto const getBalance = [](auto const& line, auto const& other, bool zero) { + STAmount amt = line ? line->at(sfBalance) : other->at(sfBalance).zeroed(); + return zero ? amt.zeroed() : amt; + }; + + /* Trust lines can be created dynamically by other transactions such as + * Payment and OfferCreate that cross offers. Such trust line won't be + * created frozen, but the sender might be, so the starting balance must be + * treated as zero. + */ + auto const balanceBefore = getBalance(before, after, false); + + /* Same as above, trust lines can be dynamically deleted, and for frozen + * trust lines, payments not involving the issuer must be blocked. This is + * achieved by treating the final balance as zero when isDelete=true to + * ensure frozen line restrictions are enforced even during deletion. + */ + auto const balanceAfter = getBalance(after, before, isDelete); + + return balanceAfter - balanceBefore; +} + +void +TransfersNotFrozen::recordBalance(Issue const& issue, BalanceChange change) +{ + XRPL_ASSERT( + change.balanceChangeSign, + "xrpl::TransfersNotFrozen::recordBalance : valid trustline " + "balance sign."); + auto& changes = balanceChanges_[issue]; + if (change.balanceChangeSign < 0) + changes.senders.emplace_back(std::move(change)); + else + changes.receivers.emplace_back(std::move(change)); +} + +void +TransfersNotFrozen::recordBalanceChanges( + std::shared_ptr const& after, + STAmount const& balanceChange) +{ + auto const balanceChangeSign = balanceChange.signum(); + auto const currency = after->at(sfBalance).getCurrency(); + + // Change from low account's perspective, which is trust line default + recordBalance({currency, after->at(sfHighLimit).getIssuer()}, {after, balanceChangeSign}); + + // Change from high account's perspective, which reverses the sign. + recordBalance({currency, after->at(sfLowLimit).getIssuer()}, {after, -balanceChangeSign}); +} + +std::shared_ptr +TransfersNotFrozen::findIssuer(AccountID const& issuerID, ReadView const& view) +{ + if (auto it = possibleIssuers_.find(issuerID); it != possibleIssuers_.end()) + { + return it->second; + } + + return view.read(keylet::account(issuerID)); +} + +bool +TransfersNotFrozen::validateIssuerChanges( + std::shared_ptr const& issuer, + IssuerChanges const& changes, + STTx const& tx, + beast::Journal const& j, + bool enforce) +{ + if (!issuer) + { + return false; + } + + bool const globalFreeze = issuer->isFlag(lsfGlobalFreeze); + if (changes.receivers.empty() || changes.senders.empty()) + { + /* If there are no receivers, then the holder(s) are returning + * their tokens to the issuer. Likewise, if there are no + * senders, then the issuer is issuing tokens to the holder(s). + * This is allowed regardless of the issuer's freeze flags. (The + * holder may have contradicting freeze flags, but that will be + * checked when the holder is treated as issuer.) + */ + return true; + } + + for (auto const& actors : {changes.senders, changes.receivers}) + { + for (auto const& change : actors) + { + bool const high = change.line->at(sfLowLimit).getIssuer() == issuer->at(sfAccount); + + if (!validateFrozenState(change, high, tx, j, enforce, globalFreeze)) + { + return false; + } + } + } + return true; +} + +bool +TransfersNotFrozen::validateFrozenState( + BalanceChange const& change, + bool high, + STTx const& tx, + beast::Journal const& j, + bool enforce, + bool globalFreeze) +{ + bool const freeze = + change.balanceChangeSign < 0 && change.line->isFlag(high ? lsfLowFreeze : lsfHighFreeze); + bool const deepFreeze = change.line->isFlag(high ? lsfLowDeepFreeze : lsfHighDeepFreeze); + bool const frozen = globalFreeze || deepFreeze || freeze; + + bool const isAMMLine = change.line->isFlag(lsfAMMNode); + + if (!frozen) + { + return true; + } + + // AMMClawbacks are allowed to override some freeze rules + if ((!isAMMLine || globalFreeze) && hasPrivilege(tx, overrideFreeze)) + { + JLOG(j.debug()) << "Invariant check allowing funds to be moved " + << (change.balanceChangeSign > 0 ? "to" : "from") + << " a frozen trustline for AMMClawback " << tx.getTransactionID(); + return true; + } + + 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, + "xrpl::TransfersNotFrozen::validateFrozenState : enforce " + "invariant."); + + if (enforce) + { + return false; + } + + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/invariants/InvariantCheck.cpp b/src/libxrpl/tx/invariants/InvariantCheck.cpp new file mode 100644 index 0000000000..79c593c57c --- /dev/null +++ b/src/libxrpl/tx/invariants/InvariantCheck.cpp @@ -0,0 +1,1009 @@ +#include +// +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl { + +#pragma push_macro("TRANSACTION") +#undef TRANSACTION + +#define TRANSACTION(tag, value, name, delegable, 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, + std::shared_ptr const&, + std::shared_ptr const&) +{ + // nothing to do +} + +bool +TransactionFeeCheck::finalize( + STTx const& tx, + TER const, + XRPAmount const fee, + ReadView const&, + beast::Journal const& j) +{ + // We should never charge a negative fee + if (fee.drops() < 0) + { + JLOG(j.fatal()) << "Invariant failed: fee paid was negative: " << fee.drops(); + return false; + } + + // We should never charge a fee that's greater than or equal to the + // entire XRP supply. + if (fee >= INITIAL_XRP) + { + JLOG(j.fatal()) << "Invariant failed: fee paid exceeds system limit: " << fee.drops(); + return false; + } + + // We should never charge more for a transaction than the transaction + // authorizes. It's possible to charge less in some circumstances. + if (fee > tx.getFieldAmount(sfFee).xrp()) + { + JLOG(j.fatal()) << "Invariant failed: fee paid is " << fee.drops() + << " exceeds fee specified in transaction."; + return false; + } + + return true; +} + +//------------------------------------------------------------------------------ + +void +XRPNotCreated::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + /* We go through all modified ledger entries, looking only at account roots, + * escrow payments, and payment channels. We remove from the total any + * previous XRP values and add to the total any new XRP values. The net + * balance of a payment channel is computed from two fields (amount and + * balance) and deletions are ignored for paychan and escrow because the + * amount fields have not been adjusted for those in the case of deletion. + */ + if (before) + { + switch (before->getType()) + { + case ltACCOUNT_ROOT: + drops_ -= (*before)[sfBalance].xrp().drops(); + break; + case ltPAYCHAN: + drops_ -= ((*before)[sfAmount] - (*before)[sfBalance]).xrp().drops(); + break; + case ltESCROW: + if (isXRP((*before)[sfAmount])) + drops_ -= (*before)[sfAmount].xrp().drops(); + break; + default: + break; + } + } + + if (after) + { + switch (after->getType()) + { + case ltACCOUNT_ROOT: + drops_ += (*after)[sfBalance].xrp().drops(); + break; + case ltPAYCHAN: + if (!isDelete) + drops_ += ((*after)[sfAmount] - (*after)[sfBalance]).xrp().drops(); + break; + case ltESCROW: + if (!isDelete && isXRP((*after)[sfAmount])) + drops_ += (*after)[sfAmount].xrp().drops(); + break; + default: + break; + } + } +} + +bool +XRPNotCreated::finalize( + STTx const& tx, + TER const, + XRPAmount const fee, + ReadView const&, + beast::Journal const& j) +{ + // The net change should never be positive, as this would mean that the + // transaction created XRP out of thin air. That's not possible. + if (drops_ > 0) + { + JLOG(j.fatal()) << "Invariant failed: XRP net change was positive: " << drops_; + return false; + } + + // The negative of the net change should be equal to actual fee charged. + if (-drops_ != fee.drops()) + { + JLOG(j.fatal()) << "Invariant failed: XRP net change of " << drops_ << " doesn't match fee " + << fee.drops(); + return false; + } + + return true; +} + +//------------------------------------------------------------------------------ + +void +XRPBalanceChecks::visitEntry( + bool, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + auto isBad = [](STAmount const& balance) { + if (!balance.native()) + return true; + + auto const drops = balance.xrp(); + + // Can't have more than the number of drops instantiated + // in the genesis ledger. + if (drops > INITIAL_XRP) + return true; + + // Can't have a negative balance (0 is OK) + if (drops < XRPAmount{0}) + return true; + + return false; + }; + + if (before && before->getType() == ltACCOUNT_ROOT) + bad_ |= isBad((*before)[sfBalance]); + + if (after && after->getType() == ltACCOUNT_ROOT) + bad_ |= isBad((*after)[sfBalance]); +} + +bool +XRPBalanceChecks::finalize( + STTx const&, + TER const, + XRPAmount const, + ReadView const&, + beast::Journal const& j) +{ + if (bad_) + { + JLOG(j.fatal()) << "Invariant failed: incorrect account XRP balance"; + return false; + } + + return true; +} + +//------------------------------------------------------------------------------ + +void +NoBadOffers::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + auto isBad = [](STAmount const& pays, STAmount const& gets) { + // An offer should never be negative + if (pays < beast::zero) + return true; + + if (gets < beast::zero) + return true; + + // Can't have an XRP to XRP offer: + return pays.native() && gets.native(); + }; + + if (before && before->getType() == ltOFFER) + bad_ |= isBad((*before)[sfTakerPays], (*before)[sfTakerGets]); + + if (after && after->getType() == ltOFFER) + bad_ |= isBad((*after)[sfTakerPays], (*after)[sfTakerGets]); +} + +bool +NoBadOffers::finalize( + STTx const&, + TER const, + XRPAmount const, + ReadView const&, + beast::Journal const& j) +{ + if (bad_) + { + JLOG(j.fatal()) << "Invariant failed: offer with a bad amount"; + return false; + } + + return true; +} + +//------------------------------------------------------------------------------ + +void +NoZeroEscrow::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + auto isBad = [](STAmount const& amount) { + // XRP case + if (amount.native()) + { + if (amount.xrp() <= XRPAmount{0}) + return true; + + if (amount.xrp() >= INITIAL_XRP) + return true; + } + else + { + // IOU case + if (amount.holds()) + { + if (amount <= beast::zero) + return true; + + if (badCurrency() == amount.getCurrency()) + return true; + } + + // MPT case + if (amount.holds()) + { + if (amount <= beast::zero) + return true; + + if (amount.mpt() > MPTAmount{maxMPTokenAmount}) + return true; // LCOV_EXCL_LINE + } + } + return false; + }; + + if (before && before->getType() == ltESCROW) + bad_ |= isBad((*before)[sfAmount]); + + if (after && after->getType() == ltESCROW) + bad_ |= isBad((*after)[sfAmount]); + + auto checkAmount = [this](std::int64_t amount) { + if (amount > maxMPTokenAmount || amount < 0) + bad_ = true; + }; + + if (after && after->getType() == ltMPTOKEN_ISSUANCE) + { + auto const outstanding = (*after)[sfOutstandingAmount]; + checkAmount(outstanding); + if (auto const locked = (*after)[~sfLockedAmount]) + { + checkAmount(*locked); + bad_ = outstanding < *locked; + } + } + + if (after && after->getType() == ltMPTOKEN) + { + auto const mptAmount = (*after)[sfMPTAmount]; + checkAmount(mptAmount); + if (auto const locked = (*after)[~sfLockedAmount]) + { + checkAmount(*locked); + } + } +} + +bool +NoZeroEscrow::finalize( + STTx const& txn, + TER const, + XRPAmount const, + ReadView const& rv, + beast::Journal const& j) +{ + if (bad_) + { + JLOG(j.fatal()) << "Invariant failed: escrow specifies invalid amount"; + return false; + } + + return true; +} + +//------------------------------------------------------------------------------ + +void +AccountRootsNotDeleted::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const&) +{ + if (isDelete && before && before->getType() == ltACCOUNT_ROOT) + accountsDeleted_++; +} + +bool +AccountRootsNotDeleted::finalize( + STTx const& tx, + TER const result, + XRPAmount const, + ReadView const&, + beast::Journal const& j) +{ + // AMM account root can be deleted as the result of AMM withdraw/delete + // transaction when the total AMM LP Tokens balance goes to 0. + // A successful AccountDelete or AMMDelete MUST delete exactly + // one account root. + if (hasPrivilege(tx, mustDeleteAcct) && result == tesSUCCESS) + { + if (accountsDeleted_ == 1) + return true; + + if (accountsDeleted_ == 0) + JLOG(j.fatal()) << "Invariant failed: account deletion " + "succeeded without deleting an account"; + else + JLOG(j.fatal()) << "Invariant failed: account deletion " + "succeeded but deleted multiple accounts!"; + return false; + } + + // 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 (hasPrivilege(tx, mayDeleteAcct) && result == tesSUCCESS && accountsDeleted_ == 1) + return true; + + if (accountsDeleted_ == 0) + return true; + + JLOG(j.fatal()) << "Invariant failed: an account root was deleted"; + return false; +} + +//------------------------------------------------------------------------------ + +void +AccountRootsDeletedClean::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (isDelete && before && before->getType() == ltACCOUNT_ROOT) + accountsDeleted_.emplace_back(before, after); +} + +bool +AccountRootsDeletedClean::finalize( + STTx const& tx, + TER const result, + XRPAmount const, + ReadView const& view, + beast::Journal const& j) +{ + // Always check for objects in the ledger, but to prevent differing + // transaction processing results, however unlikely, only fail if the + // 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(featureSingleAssetVault) || + view.rules().enabled(featureLendingProtocol); + + auto const objectExists = [&view, enforce, &j](auto const& keylet) { + (void)enforce; + if (auto const sle = view.read(keylet)) + { + // Finding the object is bad + auto const typeName = [&sle]() { + auto item = LedgerFormats::getInstance().findByType(sle->getType()); + + if (item != nullptr) + return item->getName(); + return std::to_string(sle->getType()); + }(); + + 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, + "xrpl::AccountRootsDeletedClean::finalize::objectExists : " + "account deletion left no objects behind"); + return true; + } + return false; + }; + + for (auto const& [before, after] : accountsDeleted_) + { + auto const accountID = before->getAccountID(sfAccount); + // An account should not be deleted with a balance + if (after->at(sfBalance) != beast::zero) + { + JLOG(j.fatal()) << "Invariant failed: account deletion left " + "behind a non-zero balance"; + XRPL_ASSERT( + enforce, + "xrpl::AccountRootsDeletedClean::finalize : " + "deleted account has zero balance"); + if (enforce) + return false; + } + // An account should not be deleted with a non-zero owner count + if (after->at(sfOwnerCount) != 0) + { + JLOG(j.fatal()) << "Invariant failed: account deletion left " + "behind a non-zero owner count"; + XRPL_ASSERT( + enforce, + "xrpl::AccountRootsDeletedClean::finalize : " + "deleted account has zero owner count"); + if (enforce) + return false; + } + // Simple types + for (auto const& [keyletfunc, _, __] : directAccountKeylets) + { + if (objectExists(std::invoke(keyletfunc, accountID)) && enforce) + return false; + } + + { + // NFT pages. nftpage_min and nftpage_max were already explicitly + // checked above as entries in directAccountKeylets. This uses + // view.succ() to check for any NFT pages in between the two + // endpoints. + Keylet const first = keylet::nftpage_min(accountID); + Keylet const last = keylet::nftpage_max(accountID); + + std::optional key = view.succ(first.key, last.key.next()); + + // current page + if (key && objectExists(Keylet{ltNFTOKEN_PAGE, *key}) && enforce) + return false; + } + + // 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 (before->isFieldPresent(*field)) + { + auto const key = before->getFieldH256(*field); + if (objectExists(keylet::unchecked(key)) && enforce) + return false; + } + } + } + + return true; +} + +//------------------------------------------------------------------------------ + +void +LedgerEntryTypesMatch::visitEntry( + bool, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (before && after && before->getType() != after->getType()) + typeMismatch_ = true; + + if (after) + { +#pragma push_macro("LEDGER_ENTRY") +#undef LEDGER_ENTRY + +#define LEDGER_ENTRY(tag, ...) case tag: + + switch (after->getType()) + { +#include + + break; + default: + invalidTypeAdded_ = true; + break; + } + +#undef LEDGER_ENTRY +#pragma pop_macro("LEDGER_ENTRY") + } +} + +bool +LedgerEntryTypesMatch::finalize( + STTx const&, + TER const, + XRPAmount const, + ReadView const&, + beast::Journal const& j) +{ + if ((!typeMismatch_) && (!invalidTypeAdded_)) + return true; + + if (typeMismatch_) + { + JLOG(j.fatal()) << "Invariant failed: ledger entry type mismatch"; + } + + if (invalidTypeAdded_) + { + JLOG(j.fatal()) << "Invariant failed: invalid ledger entry type added"; + } + + return false; +} + +//------------------------------------------------------------------------------ + +void +NoXRPTrustLines::visitEntry( + bool, + std::shared_ptr const&, + std::shared_ptr const& after) +{ + if (after && after->getType() == ltRIPPLE_STATE) + { + // checking the issue directly here instead of + // relying on .native() just in case native somehow + // were systematically incorrect + xrpTrustLine_ = after->getFieldAmount(sfLowLimit).issue() == xrpIssue() || + after->getFieldAmount(sfHighLimit).issue() == xrpIssue(); + } +} + +bool +NoXRPTrustLines::finalize( + STTx const&, + TER const, + XRPAmount const, + ReadView const&, + beast::Journal const& j) +{ + if (!xrpTrustLine_) + return true; + + JLOG(j.fatal()) << "Invariant failed: an XRP trust line was created"; + return false; +} + +//------------------------------------------------------------------------------ + +void +NoDeepFreezeTrustLinesWithoutFreeze::visitEntry( + bool, + std::shared_ptr const&, + std::shared_ptr const& after) +{ + if (after && after->getType() == ltRIPPLE_STATE) + { + std::uint32_t const uFlags = after->getFieldU32(sfFlags); + bool const lowFreeze = uFlags & lsfLowFreeze; + bool const lowDeepFreeze = uFlags & lsfLowDeepFreeze; + + bool const highFreeze = uFlags & lsfHighFreeze; + bool const highDeepFreeze = uFlags & lsfHighDeepFreeze; + + deepFreezeWithoutFreeze_ = (lowDeepFreeze && !lowFreeze) || (highDeepFreeze && !highFreeze); + } +} + +bool +NoDeepFreezeTrustLinesWithoutFreeze::finalize( + STTx const&, + TER const, + XRPAmount const, + ReadView const&, + beast::Journal const& j) +{ + if (!deepFreezeWithoutFreeze_) + return true; + + JLOG(j.fatal()) << "Invariant failed: a trust line with deep freeze flag " + "without normal freeze was created"; + return false; +} + +//------------------------------------------------------------------------------ + +void +ValidNewAccountRoot::visitEntry( + bool, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (!before && after->getType() == ltACCOUNT_ROOT) + { + accountsCreated_++; + accountSeq_ = (*after)[sfSequence]; + pseudoAccount_ = isPseudoAccount(after); + flags_ = after->getFlags(); + } +} + +bool +ValidNewAccountRoot::finalize( + STTx const& tx, + TER const result, + XRPAmount const, + ReadView const& view, + beast::Journal const& j) +{ + if (accountsCreated_ == 0) + return true; + + if (accountsCreated_ > 1) + { + JLOG(j.fatal()) << "Invariant failed: multiple accounts " + "created in a single transaction"; + return false; + } + + // From this point on we know exactly one account was created. + if (hasPrivilege(tx, createAcct | createPseudoAcct) && result == tesSUCCESS) + { + bool const pseudoAccount = + (pseudoAccount_ && + (view.rules().enabled(featureSingleAssetVault) || + view.rules().enabled(featureLendingProtocol))); + + if (pseudoAccount && !hasPrivilege(tx, createPseudoAcct)) + { + JLOG(j.fatal()) << "Invariant failed: pseudo-account created by a " + "wrong transaction type"; + return false; + } + + std::uint32_t const startingSeq = pseudoAccount ? 0 : view.seq(); + + if (accountSeq_ != startingSeq) + { + JLOG(j.fatal()) << "Invariant failed: account created with " + "wrong starting sequence number"; + return false; + } + + if (pseudoAccount) + { + std::uint32_t const expected = (lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth); + if (flags_ != expected) + { + JLOG(j.fatal()) << "Invariant failed: pseudo-account created with " + "wrong flags"; + return false; + } + } + + return true; + } + + JLOG(j.fatal()) << "Invariant failed: account root created illegally"; + return false; +} // namespace xrpl + +//------------------------------------------------------------------------------ + +void +ValidClawback::visitEntry( + bool, + std::shared_ptr const& before, + std::shared_ptr const&) +{ + if (before && before->getType() == ltRIPPLE_STATE) + trustlinesChanged++; + + if (before && before->getType() == ltMPTOKEN) + mptokensChanged++; +} + +bool +ValidClawback::finalize( + STTx const& tx, + TER const result, + XRPAmount const, + ReadView const& view, + beast::Journal const& j) +{ + if (tx.getTxnType() != ttCLAWBACK) + return true; + + if (result == tesSUCCESS) + { + if (trustlinesChanged > 1) + { + JLOG(j.fatal()) << "Invariant failed: more than one trustline changed."; + return false; + } + + if (mptokensChanged > 1) + { + JLOG(j.fatal()) << "Invariant failed: more than one mptokens changed."; + return false; + } + + if (trustlinesChanged == 1) + { + AccountID const issuer = tx.getAccountID(sfAccount); + STAmount const& amount = tx.getFieldAmount(sfAmount); + AccountID const& holder = amount.getIssuer(); + STAmount const holderBalance = + accountHolds(view, holder, amount.getCurrency(), issuer, fhIGNORE_FREEZE, j); + + if (holderBalance.signum() < 0) + { + JLOG(j.fatal()) << "Invariant failed: trustline balance is negative"; + return false; + } + } + } + else + { + if (trustlinesChanged != 0) + { + JLOG(j.fatal()) << "Invariant failed: some trustlines were changed " + "despite failure of the transaction."; + return false; + } + + if (mptokensChanged != 0) + { + JLOG(j.fatal()) << "Invariant failed: some mptokens were changed " + "despite failure of the transaction."; + return false; + } + } + + return 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); + XRPL_ASSERT( + errors_.empty() || enforce, + "xrpl::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 +NoModifiedUnmodifiableFields::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (isDelete || !before) + // Creation and deletion are ignored + return; + + changedEntries_.emplace(before, after); +} + +bool +NoModifiedUnmodifiableFields::finalize( + STTx const& tx, + TER const, + XRPAmount const, + ReadView const& view, + beast::Journal const& j) +{ + static auto const fieldChanged = [](auto const& before, auto const& after, auto const& field) { + bool const beforeField = before->isFieldPresent(field); + bool const afterField = after->isFieldPresent(field); + return beforeField != afterField || (afterField && before->at(field) != after->at(field)); + }; + for (auto const& slePair : changedEntries_) + { + auto const& before = slePair.first; + auto const& after = slePair.second; + auto const type = after->getType(); + bool bad = false; + [[maybe_unused]] bool enforce = false; + switch (type) + { + case ltLOAN_BROKER: + /* + * We check this invariant regardless of lending protocol + * amendment status, allowing for detection and logging of + * potential issues even when the amendment is disabled. + */ + enforce = view.rules().enabled(featureLendingProtocol); + bad = fieldChanged(before, after, sfLedgerEntryType) || + fieldChanged(before, after, sfLedgerIndex) || + fieldChanged(before, after, sfSequence) || + fieldChanged(before, after, sfOwnerNode) || + fieldChanged(before, after, sfVaultNode) || + fieldChanged(before, after, sfVaultID) || + fieldChanged(before, after, sfAccount) || + fieldChanged(before, after, sfOwner) || + fieldChanged(before, after, sfManagementFeeRate) || + fieldChanged(before, after, sfCoverRateMinimum) || + fieldChanged(before, after, sfCoverRateLiquidation); + break; + case ltLOAN: + /* + * We check this invariant regardless of lending protocol + * amendment status, allowing for detection and logging of + * potential issues even when the amendment is disabled. + */ + enforce = view.rules().enabled(featureLendingProtocol); + bad = fieldChanged(before, after, sfLedgerEntryType) || + fieldChanged(before, after, sfLedgerIndex) || + fieldChanged(before, after, sfSequence) || + fieldChanged(before, after, sfOwnerNode) || + fieldChanged(before, after, sfLoanBrokerNode) || + fieldChanged(before, after, sfLoanBrokerID) || + fieldChanged(before, after, sfBorrower) || + fieldChanged(before, after, sfLoanOriginationFee) || + fieldChanged(before, after, sfLoanServiceFee) || + fieldChanged(before, after, sfLatePaymentFee) || + fieldChanged(before, after, sfClosePaymentFee) || + fieldChanged(before, after, sfOverpaymentFee) || + fieldChanged(before, after, sfInterestRate) || + fieldChanged(before, after, sfLateInterestRate) || + fieldChanged(before, after, sfCloseInterestRate) || + fieldChanged(before, after, sfOverpaymentInterestRate) || + fieldChanged(before, after, sfStartDate) || + fieldChanged(before, after, sfPaymentInterval) || + fieldChanged(before, after, sfGracePeriod) || + fieldChanged(before, after, sfLoanScale); + break; + default: + /* + * We check this invariant regardless of lending protocol + * amendment status, allowing for detection and logging of + * potential issues even when the amendment is disabled. + * + * We use the lending protocol as a gate, even though + * all transactions are affected because that's when it + * was added. + */ + enforce = view.rules().enabled(featureLendingProtocol); + bad = fieldChanged(before, after, sfLedgerEntryType) || + fieldChanged(before, after, sfLedgerIndex); + } + XRPL_ASSERT( + !bad || enforce, + "xrpl::NoModifiedUnmodifiableFields::finalize : no bad " + "changes or enforce invariant"); + if (bad) + { + JLOG(j.fatal()) << "Invariant failed: changed an unchangeable field for " + << tx.getTransactionID(); + if (enforce) + return false; + } + } + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/invariants/LoanInvariant.cpp b/src/libxrpl/tx/invariants/LoanInvariant.cpp new file mode 100644 index 0000000000..01c4da46ac --- /dev/null +++ b/src/libxrpl/tx/invariants/LoanInvariant.cpp @@ -0,0 +1,278 @@ +#include +// +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl { + +void +ValidLoanBroker::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (after) + { + if (after->getType() == ltLOAN_BROKER) + { + auto& broker = brokers_[after->key()]; + broker.brokerBefore = before; + broker.brokerAfter = after; + } + else if (after->getType() == ltACCOUNT_ROOT && after->isFieldPresent(sfLoanBrokerID)) + { + auto const& loanBrokerID = after->at(sfLoanBrokerID); + // create an entry if one doesn't already exist + brokers_.emplace(loanBrokerID, BrokerInfo{}); + } + else if (after->getType() == ltRIPPLE_STATE) + { + lines_.emplace_back(after); + } + else if (after->getType() == ltMPTOKEN) + { + mpts_.emplace_back(after); + } + } +} + +bool +ValidLoanBroker::goodZeroDirectory( + ReadView const& view, + SLE::const_ref dir, + beast::Journal const& j) const +{ + auto const next = dir->at(~sfIndexNext); + auto const prev = dir->at(~sfIndexPrevious); + if ((prev && *prev) || (next && *next)) + { + JLOG(j.fatal()) << "Invariant failed: Loan Broker with zero " + "OwnerCount has multiple directory pages"; + return false; + } + auto indexes = dir->getFieldV256(sfIndexes); + if (indexes.size() > 1) + { + JLOG(j.fatal()) << "Invariant failed: Loan Broker with zero " + "OwnerCount has multiple indexes in the Directory root"; + return false; + } + if (indexes.size() == 1) + { + auto const index = indexes.value().front(); + auto const sle = view.read(keylet::unchecked(index)); + if (!sle) + { + JLOG(j.fatal()) << "Invariant failed: Loan Broker directory corrupt"; + return false; + } + if (sle->getType() != ltRIPPLE_STATE && sle->getType() != ltMPTOKEN) + { + JLOG(j.fatal()) << "Invariant failed: Loan Broker with zero " + "OwnerCount has an unexpected entry in the directory"; + return false; + } + } + + return true; +} + +bool +ValidLoanBroker::finalize( + STTx const& tx, + TER const, + XRPAmount const, + ReadView const& view, + beast::Journal const& j) +{ + // Loan Brokers will not exist on ledger if the Lending Protocol amendment + // is not enabled, so there's no need to check it. + + for (auto const& line : lines_) + { + for (auto const& field : {&sfLowLimit, &sfHighLimit}) + { + auto const account = view.read(keylet::account(line->at(*field).getIssuer())); + // This Invariant doesn't know about the rules for Trust Lines, so + // if the account is missing, don't treat it as an error. This + // loop is only concerned with finding Broker pseudo-accounts + if (account && account->isFieldPresent(sfLoanBrokerID)) + { + auto const& loanBrokerID = account->at(sfLoanBrokerID); + // create an entry if one doesn't already exist + brokers_.emplace(loanBrokerID, BrokerInfo{}); + } + } + } + for (auto const& mpt : mpts_) + { + auto const account = view.read(keylet::account(mpt->at(sfAccount))); + // This Invariant doesn't know about the rules for MPTokens, so + // if the account is missing, don't treat is as an error. This + // loop is only concerned with finding Broker pseudo-accounts + if (account && account->isFieldPresent(sfLoanBrokerID)) + { + auto const& loanBrokerID = account->at(sfLoanBrokerID); + // create an entry if one doesn't already exist + brokers_.emplace(loanBrokerID, BrokerInfo{}); + } + } + + for (auto const& [brokerID, broker] : brokers_) + { + auto const& after = + broker.brokerAfter ? broker.brokerAfter : view.read(keylet::loanbroker(brokerID)); + + if (!after) + { + JLOG(j.fatal()) << "Invariant failed: Loan Broker missing"; + return false; + } + + auto const& before = broker.brokerBefore; + + // https://github.com/Tapanito/XRPL-Standards/blob/xls-66-lending-protocol/XLS-0066d-lending-protocol/README.md#3123-invariants + // If `LoanBroker.OwnerCount = 0` the `DirectoryNode` will have at most + // one node (the root), which will only hold entries for `RippleState` + // or `MPToken` objects. + if (after->at(sfOwnerCount) == 0) + { + auto const dir = view.read(keylet::ownerDir(after->at(sfAccount))); + if (dir) + { + if (!goodZeroDirectory(view, dir, j)) + { + return false; + } + } + } + if (before && before->at(sfLoanSequence) > after->at(sfLoanSequence)) + { + JLOG(j.fatal()) << "Invariant failed: Loan Broker sequence number " + "decreased"; + return false; + } + if (after->at(sfDebtTotal) < 0) + { + JLOG(j.fatal()) << "Invariant failed: Loan Broker debt total is negative"; + return false; + } + if (after->at(sfCoverAvailable) < 0) + { + JLOG(j.fatal()) << "Invariant failed: Loan Broker cover available is negative"; + return false; + } + auto const vault = view.read(keylet::vault(after->at(sfVaultID))); + if (!vault) + { + JLOG(j.fatal()) << "Invariant failed: Loan Broker vault ID is invalid"; + return false; + } + auto const& vaultAsset = vault->at(sfAsset); + if (after->at(sfCoverAvailable) < accountHolds( + view, + after->at(sfAccount), + vaultAsset, + FreezeHandling::fhIGNORE_FREEZE, + AuthHandling::ahIGNORE_AUTH, + j)) + { + JLOG(j.fatal()) << "Invariant failed: Loan Broker cover available " + "is less than pseudo-account asset balance"; + return false; + } + } + return true; +} + +//------------------------------------------------------------------------------ + +void +ValidLoan::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (after && after->getType() == ltLOAN) + { + loans_.emplace_back(before, after); + } +} + +bool +ValidLoan::finalize( + STTx const& tx, + TER const, + XRPAmount const, + ReadView const& view, + beast::Journal const& j) +{ + // Loans will not exist on ledger if the Lending Protocol amendment + // is not enabled, so there's no need to check it. + + for (auto const& [before, after] : loans_) + { + // https://github.com/Tapanito/XRPL-Standards/blob/xls-66-lending-protocol/XLS-0066d-lending-protocol/README.md#3223-invariants + // If `Loan.PaymentRemaining = 0` then the loan MUST be fully paid off + if (after->at(sfPaymentRemaining) == 0 && + (after->at(sfTotalValueOutstanding) != beast::zero || + after->at(sfPrincipalOutstanding) != beast::zero || + after->at(sfManagementFeeOutstanding) != beast::zero)) + { + JLOG(j.fatal()) << "Invariant failed: Loan with zero payments " + "remaining has not been paid off"; + return false; + } + // If `Loan.PaymentRemaining != 0` then the loan MUST NOT be fully paid + // off + if (after->at(sfPaymentRemaining) != 0 && + after->at(sfTotalValueOutstanding) == beast::zero && + after->at(sfPrincipalOutstanding) == beast::zero && + after->at(sfManagementFeeOutstanding) == beast::zero) + { + JLOG(j.fatal()) << "Invariant failed: Loan with zero payments " + "remaining has not been paid off"; + return false; + } + if (before && (before->isFlag(lsfLoanOverpayment) != after->isFlag(lsfLoanOverpayment))) + { + JLOG(j.fatal()) << "Invariant failed: Loan Overpayment flag changed"; + return false; + } + // Must not be negative - STNumber + for (auto const field : + {&sfLoanServiceFee, + &sfLatePaymentFee, + &sfClosePaymentFee, + &sfPrincipalOutstanding, + &sfTotalValueOutstanding, + &sfManagementFeeOutstanding}) + { + if (after->at(*field) < 0) + { + JLOG(j.fatal()) << "Invariant failed: " << field->getName() << " is negative "; + return false; + } + } + // Must be positive - STNumber + for (auto const field : { + &sfPeriodicPayment, + }) + { + if (after->at(*field) <= 0) + { + JLOG(j.fatal()) << "Invariant failed: " << field->getName() + << " is zero or negative "; + return false; + } + } + } + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/invariants/MPTInvariant.cpp b/src/libxrpl/tx/invariants/MPTInvariant.cpp new file mode 100644 index 0000000000..20957b8d43 --- /dev/null +++ b/src/libxrpl/tx/invariants/MPTInvariant.cpp @@ -0,0 +1,192 @@ +#include +// +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl { + +void +ValidMPTIssuance::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (after && after->getType() == ltMPTOKEN_ISSUANCE) + { + if (isDelete) + mptIssuancesDeleted_++; + else if (!before) + mptIssuancesCreated_++; + } + + if (after && after->getType() == ltMPTOKEN) + { + if (isDelete) + mptokensDeleted_++; + else if (!before) + { + mptokensCreated_++; + MPTIssue const mptIssue{after->at(sfMPTokenIssuanceID)}; + if (mptIssue.getIssuer() == after->at(sfAccount)) + mptCreatedByIssuer_ = true; + } + } +} + +bool +ValidMPTIssuance::finalize( + STTx const& tx, + TER const result, + XRPAmount const _fee, + ReadView const& view, + beast::Journal const& j) +{ + if (result == tesSUCCESS) + { + auto const& rules = view.rules(); + [[maybe_unused]] + bool enforceCreatedByIssuer = + rules.enabled(featureSingleAssetVault) || rules.enabled(featureLendingProtocol); + if (mptCreatedByIssuer_) + { + JLOG(j.fatal()) << "Invariant failed: MPToken created for the MPT issuer"; + // The comment above starting with "assert(enforce)" explains this + // assert. + XRPL_ASSERT_PARTS( + enforceCreatedByIssuer, "xrpl::ValidMPTIssuance::finalize", "no issuer MPToken"); + if (enforceCreatedByIssuer) + return false; + } + + auto const txnType = tx.getTxnType(); + if (hasPrivilege(tx, createMPTIssuance)) + { + if (mptIssuancesCreated_ == 0) + { + JLOG(j.fatal()) << "Invariant failed: transaction " + "succeeded without creating a MPT issuance"; + } + else if (mptIssuancesDeleted_ != 0) + { + JLOG(j.fatal()) << "Invariant failed: transaction " + "succeeded while removing MPT issuances"; + } + else if (mptIssuancesCreated_ > 1) + { + JLOG(j.fatal()) << "Invariant failed: transaction " + "succeeded but created multiple issuances"; + } + + return mptIssuancesCreated_ == 1 && mptIssuancesDeleted_ == 0; + } + + if (hasPrivilege(tx, destroyMPTIssuance)) + { + if (mptIssuancesDeleted_ == 0) + { + JLOG(j.fatal()) << "Invariant failed: MPT issuance deletion " + "succeeded without removing a MPT issuance"; + } + else if (mptIssuancesCreated_ > 0) + { + JLOG(j.fatal()) << "Invariant failed: MPT issuance deletion " + "succeeded while creating MPT issuances"; + } + else if (mptIssuancesDeleted_ > 1) + { + JLOG(j.fatal()) << "Invariant failed: MPT issuance deletion " + "succeeded but deleted multiple issuances"; + } + + return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 1; + } + + bool const lendingProtocolEnabled = view.rules().enabled(featureLendingProtocol); + // 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 = (txnType == ttESCROW_FINISH) && + (view.rules().enabled(featureSingleAssetVault) || lendingProtocolEnabled); + if (hasPrivilege(tx, mustAuthorizeMPT | mayAuthorizeMPT) || enforceEscrowFinish) + { + bool const submittedByIssuer = tx.isFieldPresent(sfHolder); + + if (mptIssuancesCreated_ > 0) + { + JLOG(j.fatal()) << "Invariant failed: MPT authorize " + "succeeded but created MPT issuances"; + return false; + } + else if (mptIssuancesDeleted_ > 0) + { + JLOG(j.fatal()) << "Invariant failed: MPT authorize " + "succeeded but deleted issuances"; + return false; + } + else if (lendingProtocolEnabled && mptokensCreated_ + mptokensDeleted_ > 1) + { + JLOG(j.fatal()) << "Invariant failed: MPT authorize succeeded " + "but created/deleted bad number mptokens"; + return false; + } + else if (submittedByIssuer && (mptokensCreated_ > 0 || mptokensDeleted_ > 0)) + { + JLOG(j.fatal()) << "Invariant failed: MPT authorize submitted by issuer " + "succeeded but created/deleted mptokens"; + return false; + } + else if ( + !submittedByIssuer && hasPrivilege(tx, mustAuthorizeMPT) && + (mptokensCreated_ + mptokensDeleted_ != 1)) + { + // if the holder submitted this tx, then a mptoken must be + // either created or deleted. + JLOG(j.fatal()) << "Invariant failed: MPT authorize submitted by holder " + "succeeded but created/deleted bad number of mptokens"; + return false; + } + + return true; + } + if (txnType == ttESCROW_FINISH) + { + // 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, "xrpl::ValidMPTIssuance::finalize", "not escrow finish tx"); + return true; + } + + if (hasPrivilege(tx, mayDeleteMPT) && mptokensDeleted_ == 1 && mptokensCreated_ == 0 && + mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0) + return true; + } + + if (mptIssuancesCreated_ != 0) + { + JLOG(j.fatal()) << "Invariant failed: a MPT issuance was created"; + } + else if (mptIssuancesDeleted_ != 0) + { + JLOG(j.fatal()) << "Invariant failed: a MPT issuance was deleted"; + } + else if (mptokensCreated_ != 0) + { + JLOG(j.fatal()) << "Invariant failed: a MPToken was created"; + } + else if (mptokensDeleted_ != 0) + { + JLOG(j.fatal()) << "Invariant failed: a MPToken was deleted"; + } + + return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0 && mptokensCreated_ == 0 && + mptokensDeleted_ == 0; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/invariants/NFTInvariant.cpp b/src/libxrpl/tx/invariants/NFTInvariant.cpp new file mode 100644 index 0000000000..db06896023 --- /dev/null +++ b/src/libxrpl/tx/invariants/NFTInvariant.cpp @@ -0,0 +1,274 @@ +#include +// +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl { + +void +ValidNFTokenPage::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + static constexpr uint256 const& pageBits = nft::pageMask; + static constexpr uint256 const accountBits = ~pageBits; + + if ((before && before->getType() != ltNFTOKEN_PAGE) || + (after && after->getType() != ltNFTOKEN_PAGE)) + return; + + auto check = [this, isDelete](std::shared_ptr const& sle) { + uint256 const account = sle->key() & accountBits; + uint256 const hiLimit = sle->key() & pageBits; + std::optional const prev = (*sle)[~sfPreviousPageMin]; + + // Make sure that any page links... + // 1. Are properly associated with the owning account and + // 2. The page is correctly ordered between links. + if (prev) + { + if (account != (*prev & accountBits)) + badLink_ = true; + + if (hiLimit <= (*prev & pageBits)) + badLink_ = true; + } + + if (auto const next = (*sle)[~sfNextPageMin]) + { + if (account != (*next & accountBits)) + badLink_ = true; + + if (hiLimit >= (*next & pageBits)) + badLink_ = true; + } + + { + auto const& nftokens = sle->getFieldArray(sfNFTokens); + + // An NFTokenPage should never contain too many tokens or be empty. + if (std::size_t const nftokenCount = nftokens.size(); + (!isDelete && nftokenCount == 0) || nftokenCount > dirMaxTokensPerPage) + invalidSize_ = true; + + // If prev is valid, use it to establish a lower bound for + // page entries. If prev is not valid the lower bound is zero. + uint256 const loLimit = prev ? *prev & pageBits : uint256(beast::zero); + + // Also verify that all NFTokenIDs in the page are sorted. + uint256 loCmp = loLimit; + for (auto const& obj : nftokens) + { + uint256 const tokenID = obj[sfNFTokenID]; + if (!nft::compareTokens(loCmp, tokenID)) + badSort_ = true; + loCmp = tokenID; + + // None of the NFTs on this page should belong on lower or + // higher pages. + if (uint256 const tokenPageBits = tokenID & pageBits; + tokenPageBits < loLimit || tokenPageBits >= hiLimit) + badEntry_ = true; + + if (auto uri = obj[~sfURI]; uri && uri->empty()) + badURI_ = true; + } + } + }; + + if (before) + { + check(before); + + // While an account's NFToken directory contains any NFTokens, the last + // NFTokenPage (with 96 bits of 1 in the low part of the index) should + // never be deleted. + if (isDelete && (before->key() & nft::pageMask) == nft::pageMask && + before->isFieldPresent(sfPreviousPageMin)) + { + deletedFinalPage_ = true; + } + } + + if (after) + check(after); + + if (!isDelete && before && after) + { + // If the NFTokenPage + // 1. Has a NextMinPage field in before, but loses it in after, and + // 2. This is not the last page in the directory + // Then we have identified a corruption in the links between the + // NFToken pages in the NFToken directory. + if ((before->key() & nft::pageMask) != nft::pageMask && + before->isFieldPresent(sfNextPageMin) && !after->isFieldPresent(sfNextPageMin)) + { + deletedLink_ = true; + } + } +} + +bool +ValidNFTokenPage::finalize( + STTx const& tx, + TER const result, + XRPAmount const, + ReadView const& view, + beast::Journal const& j) +{ + if (badLink_) + { + JLOG(j.fatal()) << "Invariant failed: NFT page is improperly linked."; + return false; + } + + if (badEntry_) + { + JLOG(j.fatal()) << "Invariant failed: NFT found in incorrect page."; + return false; + } + + if (badSort_) + { + JLOG(j.fatal()) << "Invariant failed: NFTs on page are not sorted."; + return false; + } + + if (badURI_) + { + JLOG(j.fatal()) << "Invariant failed: NFT contains empty URI."; + return false; + } + + if (invalidSize_) + { + JLOG(j.fatal()) << "Invariant failed: NFT page has invalid size."; + return false; + } + + if (view.rules().enabled(fixNFTokenPageLinks)) + { + if (deletedFinalPage_) + { + JLOG(j.fatal()) << "Invariant failed: Last NFT page deleted with " + "non-empty directory."; + return false; + } + if (deletedLink_) + { + JLOG(j.fatal()) << "Invariant failed: Lost NextMinPage link."; + return false; + } + } + + return true; +} + +//------------------------------------------------------------------------------ +void +NFTokenCountTracking::visitEntry( + bool, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (before && before->getType() == ltACCOUNT_ROOT) + { + beforeMintedTotal += (*before)[~sfMintedNFTokens].value_or(0); + beforeBurnedTotal += (*before)[~sfBurnedNFTokens].value_or(0); + } + + if (after && after->getType() == ltACCOUNT_ROOT) + { + afterMintedTotal += (*after)[~sfMintedNFTokens].value_or(0); + afterBurnedTotal += (*after)[~sfBurnedNFTokens].value_or(0); + } +} + +bool +NFTokenCountTracking::finalize( + STTx const& tx, + TER const result, + XRPAmount const, + ReadView const& view, + beast::Journal const& j) +{ + if (!hasPrivilege(tx, changeNFTCounts)) + { + if (beforeMintedTotal != afterMintedTotal) + { + JLOG(j.fatal()) << "Invariant failed: the number of minted tokens " + "changed without a mint transaction!"; + return false; + } + + if (beforeBurnedTotal != afterBurnedTotal) + { + JLOG(j.fatal()) << "Invariant failed: the number of burned tokens " + "changed without a burn transaction!"; + return false; + } + + return true; + } + + if (tx.getTxnType() == ttNFTOKEN_MINT) + { + if (result == tesSUCCESS && beforeMintedTotal >= afterMintedTotal) + { + JLOG(j.fatal()) << "Invariant failed: successful minting didn't increase " + "the number of minted tokens."; + return false; + } + + if (result != tesSUCCESS && beforeMintedTotal != afterMintedTotal) + { + JLOG(j.fatal()) << "Invariant failed: failed minting changed the " + "number of minted tokens."; + return false; + } + + if (beforeBurnedTotal != afterBurnedTotal) + { + JLOG(j.fatal()) << "Invariant failed: minting changed the number of " + "burned tokens."; + return false; + } + } + + if (tx.getTxnType() == ttNFTOKEN_BURN) + { + if (result == tesSUCCESS) + { + if (beforeBurnedTotal >= afterBurnedTotal) + { + JLOG(j.fatal()) << "Invariant failed: successful burning didn't increase " + "the number of burned tokens."; + return false; + } + } + + if (result != tesSUCCESS && beforeBurnedTotal != afterBurnedTotal) + { + JLOG(j.fatal()) << "Invariant failed: failed burning changed the " + "number of burned tokens."; + return false; + } + + if (beforeMintedTotal != afterMintedTotal) + { + JLOG(j.fatal()) << "Invariant failed: burning changed the number of " + "minted tokens."; + return false; + } + } + + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/invariants/PermissionedDEXInvariant.cpp b/src/libxrpl/tx/invariants/PermissionedDEXInvariant.cpp new file mode 100644 index 0000000000..2ece1f3fc0 --- /dev/null +++ b/src/libxrpl/tx/invariants/PermissionedDEXInvariant.cpp @@ -0,0 +1,93 @@ +#include +// +#include +#include +#include +#include +#include +#include + +namespace xrpl { + +void +ValidPermissionedDEX::visitEntry( + bool, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (after && after->getType() == ltDIR_NODE) + { + if (after->isFieldPresent(sfDomainID)) + domains_.insert(after->getFieldH256(sfDomainID)); + } + + if (after && after->getType() == ltOFFER) + { + if (after->isFieldPresent(sfDomainID)) + domains_.insert(after->getFieldH256(sfDomainID)); + else + regularOffers_ = true; + + // if a hybrid offer is missing domain or additional book, there's + // something wrong + if (after->isFlag(lsfHybrid) && + (!after->isFieldPresent(sfDomainID) || !after->isFieldPresent(sfAdditionalBooks) || + after->getFieldArray(sfAdditionalBooks).size() > 1)) + badHybrids_ = true; + } +} + +bool +ValidPermissionedDEX::finalize( + STTx const& tx, + TER const result, + XRPAmount const, + ReadView const& view, + beast::Journal const& j) +{ + auto const txType = tx.getTxnType(); + if ((txType != ttPAYMENT && txType != ttOFFER_CREATE) || result != tesSUCCESS) + return true; + + // For each offercreate transaction, check if + // permissioned offers are valid + if (txType == ttOFFER_CREATE && badHybrids_) + { + JLOG(j.fatal()) << "Invariant failed: hybrid offer is malformed"; + return false; + } + + if (!tx.isFieldPresent(sfDomainID)) + return true; + + auto const domain = tx.getFieldH256(sfDomainID); + + if (!view.exists(keylet::permissionedDomain(domain))) + { + JLOG(j.fatal()) << "Invariant failed: domain doesn't exist"; + return false; + } + + // for both payment and offercreate, there shouldn't be another domain + // that's different from the domain specified + for (auto const& d : domains_) + { + if (d != domain) + { + JLOG(j.fatal()) << "Invariant failed: transaction" + " consumed wrong domains"; + return false; + } + } + + if (regularOffers_) + { + JLOG(j.fatal()) << "Invariant failed: domain transaction" + " affected regular offers"; + return false; + } + + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/invariants/PermissionedDomainInvariant.cpp b/src/libxrpl/tx/invariants/PermissionedDomainInvariant.cpp new file mode 100644 index 0000000000..77acbe12c6 --- /dev/null +++ b/src/libxrpl/tx/invariants/PermissionedDomainInvariant.cpp @@ -0,0 +1,162 @@ +#include +// +#include +#include +#include +#include +#include + +namespace xrpl { + +void +ValidPermissionedDomain::visitEntry( + bool isDel, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (before && before->getType() != ltPERMISSIONED_DOMAIN) + return; + if (after && after->getType() != ltPERMISSIONED_DOMAIN) + return; + + auto check = [isDel](std::vector& sleStatus, std::shared_ptr const& sle) { + auto const& credentials = sle->getFieldArray(sfAcceptedCredentials); + auto const sorted = credentials::makeSorted(credentials); + + SleStatus ss{credentials.size(), false, !sorted.empty(), isDel}; + + // If array have duplicates then all the other checks are invalid + if (ss.isUnique_) + { + unsigned i = 0; + for (auto const& cred : sorted) + { + auto const& credTx = credentials[i++]; + ss.isSorted_ = + (cred.first == credTx[sfIssuer]) && (cred.second == credTx[sfCredentialType]); + if (!ss.isSorted_) + break; + } + } + sleStatus.emplace_back(std::move(ss)); + }; + + if (after) + check(sleStatus_, after); +} + +bool +ValidPermissionedDomain::finalize( + STTx const& tx, + TER const result, + XRPAmount const, + ReadView const& view, + beast::Journal const& j) +{ + auto check = [](SleStatus const& sleStatus, beast::Journal const& j) { + if (!sleStatus.credentialsSize_) + { + JLOG(j.fatal()) << "Invariant failed: permissioned domain with " + "no rules."; + return false; + } + + if (sleStatus.credentialsSize_ > maxPermissionedDomainCredentialsArraySize) + { + JLOG(j.fatal()) << "Invariant failed: permissioned domain bad " + "credentials size " + << sleStatus.credentialsSize_; + return false; + } + + if (!sleStatus.isUnique_) + { + JLOG(j.fatal()) << "Invariant failed: permissioned domain credentials " + "aren't unique"; + return false; + } + + if (!sleStatus.isSorted_) + { + JLOG(j.fatal()) << "Invariant failed: permissioned domain credentials " + "aren't sorted"; + return false; + } + + return true; + }; + + if (view.rules().enabled(fixPermissionedDomainInvariant)) + { + // No permissioned domains should be affected if the transaction failed + if (result != tesSUCCESS) + // If nothing changed, all is good. If there were changes, that's + // bad. + return sleStatus_.empty(); + + if (sleStatus_.size() > 1) + { + JLOG(j.fatal()) << "Invariant failed: transaction affected more " + "than 1 permissioned domain entry."; + return false; + } + + switch (tx.getTxnType()) + { + case ttPERMISSIONED_DOMAIN_SET: { + if (sleStatus_.empty()) + { + JLOG(j.fatal()) << "Invariant failed: no domain objects affected by " + "PermissionedDomainSet"; + return false; + } + + auto const& sleStatus = sleStatus_[0]; + if (sleStatus.isDelete_) + { + JLOG(j.fatal()) << "Invariant failed: domain object " + "deleted by PermissionedDomainSet"; + return false; + } + return check(sleStatus, j); + } + case ttPERMISSIONED_DOMAIN_DELETE: { + if (sleStatus_.empty()) + { + JLOG(j.fatal()) << "Invariant failed: no domain objects affected by " + "PermissionedDomainDelete"; + return false; + } + + if (!sleStatus_[0].isDelete_) + { + JLOG(j.fatal()) << "Invariant failed: domain object " + "modified, but not deleted by " + "PermissionedDomainDelete"; + return false; + } + return true; + } + default: { + if (!sleStatus_.empty()) + { + JLOG(j.fatal()) << "Invariant failed: " << sleStatus_.size() + << " domain object(s) affected by an " + "unauthorized transaction. " + << tx.getTxnType(); + return false; + } + return true; + } + } + } + else + { + if (tx.getTxnType() != ttPERMISSIONED_DOMAIN_SET || result != tesSUCCESS || + sleStatus_.empty()) + return true; + return check(sleStatus_[0], j); + } +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/invariants/VaultInvariant.cpp b/src/libxrpl/tx/invariants/VaultInvariant.cpp new file mode 100644 index 0000000000..c3db3a563a --- /dev/null +++ b/src/libxrpl/tx/invariants/VaultInvariant.cpp @@ -0,0 +1,926 @@ +#include +// +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl { + +ValidVault::Vault +ValidVault::Vault::make(SLE const& from) +{ + XRPL_ASSERT(from.getType() == ltVAULT, "ValidVault::Vault::make : from Vault object"); + + ValidVault::Vault self; + self.key = from.key(); + self.asset = from.at(sfAsset); + self.pseudoId = from.getAccountID(sfAccount); + self.owner = from.at(sfOwner); + self.shareMPTID = from.getFieldH192(sfShareMPTID); + self.assetsTotal = from.at(sfAssetsTotal); + self.assetsAvailable = from.at(sfAssetsAvailable); + self.assetsMaximum = from.at(sfAssetsMaximum); + self.lossUnrealized = from.at(sfLossUnrealized); + return self; +} + +ValidVault::Shares +ValidVault::Shares::make(SLE const& from) +{ + XRPL_ASSERT( + from.getType() == ltMPTOKEN_ISSUANCE, + "ValidVault::Shares::make : from MPTokenIssuance object"); + + ValidVault::Shares self; + self.share = MPTIssue(makeMptID(from.getFieldU32(sfSequence), from.getAccountID(sfIssuer))); + self.sharesTotal = from.at(sfOutstandingAmount); + self.sharesMaximum = from[~sfMaximumAmount].value_or(maxMPTokenAmount); + return self; +} + +void +ValidVault::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + // If `before` is empty, this means an object is being created, in which + // case `isDelete` must be false. Otherwise `before` and `after` are set and + // `isDelete` indicates whether an object is being deleted or modified. + XRPL_ASSERT( + after != nullptr && (before != nullptr || !isDelete), + "xrpl::ValidVault::visitEntry : some object is available"); + + // Number balanceDelta will capture the difference (delta) between "before" + // state (zero if created) and "after" state (zero if destroyed), so the + // invariants can validate that the change in account balances matches the + // change in vault balances, stored to deltas_ at the end of this function. + Number balanceDelta{}; + + std::int8_t sign = 0; + if (before) + { + switch (before->getType()) + { + case ltVAULT: + beforeVault_.push_back(Vault::make(*before)); + break; + case ltMPTOKEN_ISSUANCE: + // At this moment we have no way of telling if this object holds + // vault shares or something else. Save it for finalize. + beforeMPTs_.push_back(Shares::make(*before)); + balanceDelta = static_cast(before->getFieldU64(sfOutstandingAmount)); + sign = 1; + break; + case ltMPTOKEN: + balanceDelta = static_cast(before->getFieldU64(sfMPTAmount)); + sign = -1; + break; + case ltACCOUNT_ROOT: + case ltRIPPLE_STATE: + balanceDelta = before->getFieldAmount(sfBalance); + sign = -1; + break; + default:; + } + } + + if (!isDelete && after) + { + switch (after->getType()) + { + case ltVAULT: + afterVault_.push_back(Vault::make(*after)); + break; + case ltMPTOKEN_ISSUANCE: + // At this moment we have no way of telling if this object holds + // vault shares or something else. Save it for finalize. + afterMPTs_.push_back(Shares::make(*after)); + balanceDelta -= + Number(static_cast(after->getFieldU64(sfOutstandingAmount))); + sign = 1; + break; + case ltMPTOKEN: + balanceDelta -= Number(static_cast(after->getFieldU64(sfMPTAmount))); + sign = -1; + break; + case ltACCOUNT_ROOT: + case ltRIPPLE_STATE: + balanceDelta -= Number(after->getFieldAmount(sfBalance)); + sign = -1; + break; + default:; + } + } + + uint256 const key = (before ? before->key() : after->key()); + // Append to deltas if sign is non-zero, i.e. an object of an interesting + // type has been updated. A transaction may update an object even when + // its balance has not changed, e.g. transaction fee equals the amount + // transferred to the account. We intentionally do not compare balanceDelta + // against zero, to avoid missing such updates. + if (sign != 0) + deltas_[key] = balanceDelta * sign; +} + +bool +ValidVault::finalize( + STTx const& tx, + TER const ret, + XRPAmount const fee, + ReadView const& view, + beast::Journal const& j) +{ + bool const enforce = view.rules().enabled(featureSingleAssetVault); + + if (!isTesSuccess(ret)) + return true; // Do not perform checks + + if (afterVault_.empty() && beforeVault_.empty()) + { + if (hasPrivilege(tx, mustModifyVault)) + { + JLOG(j.fatal()) << // + "Invariant failed: vault operation succeeded without modifying " + "a vault"; + XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault noop invariant"); + return !enforce; + } + + return true; // Not a vault operation + } + else if (!(hasPrivilege(tx, mustModifyVault) || hasPrivilege(tx, mayModifyVault))) + { + JLOG(j.fatal()) << // + "Invariant failed: vault updated by a wrong transaction type"; + XRPL_ASSERT( + enforce, + "xrpl::ValidVault::finalize : illegal vault transaction " + "invariant"); + return !enforce; // Also not a vault operation + } + + if (beforeVault_.size() > 1 || afterVault_.size() > 1) + { + JLOG(j.fatal()) << // + "Invariant failed: vault operation updated more than single vault"; + XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : single vault invariant"); + return !enforce; // That's all we can do here + } + + auto const txnType = tx.getTxnType(); + + // We do special handling for ttVAULT_DELETE first, because it's the only + // vault-modifying transaction without an "after" state of the vault + if (afterVault_.empty()) + { + if (txnType != ttVAULT_DELETE) + { + JLOG(j.fatal()) << // + "Invariant failed: vault deleted by a wrong transaction type"; + XRPL_ASSERT( + enforce, + "xrpl::ValidVault::finalize : illegal vault deletion " + "invariant"); + return !enforce; // That's all we can do here + } + + // Note, if afterVault_ is empty then we know that beforeVault_ is not + // empty, as enforced at the top of this function + auto const& beforeVault = beforeVault_[0]; + + // At this moment we only know a vault is being deleted and there + // might be some MPTokenIssuance objects which are deleted in the + // same transaction. Find the one matching this vault. + auto const deletedShares = [&]() -> std::optional { + for (auto const& e : beforeMPTs_) + { + if (e.share.getMptID() == beforeVault.shareMPTID) + return std::move(e); + } + return std::nullopt; + }(); + + if (!deletedShares) + { + JLOG(j.fatal()) << "Invariant failed: deleted vault must also " + "delete shares"; + XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : shares deletion invariant"); + return !enforce; // That's all we can do here + } + + bool result = true; + if (deletedShares->sharesTotal != 0) + { + JLOG(j.fatal()) << "Invariant failed: deleted vault must have no " + "shares outstanding"; + result = false; + } + if (beforeVault.assetsTotal != zero) + { + JLOG(j.fatal()) << "Invariant failed: deleted vault must have no " + "assets outstanding"; + result = false; + } + if (beforeVault.assetsAvailable != zero) + { + JLOG(j.fatal()) << "Invariant failed: deleted vault must have no " + "assets available"; + result = false; + } + + return result; + } + else if (txnType == ttVAULT_DELETE) + { + JLOG(j.fatal()) << "Invariant failed: vault deletion succeeded without " + "deleting a vault"; + XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault deletion invariant"); + return !enforce; // That's all we can do here + } + + // Note, `afterVault_.empty()` is handled above + auto const& afterVault = afterVault_[0]; + XRPL_ASSERT( + beforeVault_.empty() || beforeVault_[0].key == afterVault.key, + "xrpl::ValidVault::finalize : single vault operation"); + + auto const updatedShares = [&]() -> std::optional { + // At this moment we only know that a vault is being updated and there + // might be some MPTokenIssuance objects which are also updated in the + // same transaction. Find the one matching the shares to this vault. + // Note, we expect updatedMPTs collection to be extremely small. For + // such collections linear search is faster than lookup. + for (auto const& e : afterMPTs_) + { + if (e.share.getMptID() == afterVault.shareMPTID) + return e; + } + + auto const sleShares = view.read(keylet::mptIssuance(afterVault.shareMPTID)); + + return sleShares ? std::optional(Shares::make(*sleShares)) : std::nullopt; + }(); + + bool result = true; + + // Universal transaction checks + if (!beforeVault_.empty()) + { + auto const& beforeVault = beforeVault_[0]; + if (afterVault.asset != beforeVault.asset || afterVault.pseudoId != beforeVault.pseudoId || + afterVault.shareMPTID != beforeVault.shareMPTID) + { + JLOG(j.fatal()) << "Invariant failed: violation of vault immutable data"; + result = false; + } + } + + if (!updatedShares) + { + JLOG(j.fatal()) << "Invariant failed: updated vault must have shares"; + XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault has shares invariant"); + return !enforce; // That's all we can do here + } + + if (updatedShares->sharesTotal == 0) + { + if (afterVault.assetsTotal != zero) + { + JLOG(j.fatal()) << "Invariant failed: updated zero sized " + "vault must have no assets outstanding"; + result = false; + } + if (afterVault.assetsAvailable != zero) + { + JLOG(j.fatal()) << "Invariant failed: updated zero sized " + "vault must have no assets available"; + result = false; + } + } + else if (updatedShares->sharesTotal > updatedShares->sharesMaximum) + { + JLOG(j.fatal()) // + << "Invariant failed: updated shares must not exceed maximum " + << updatedShares->sharesMaximum; + result = false; + } + + if (afterVault.assetsAvailable < zero) + { + JLOG(j.fatal()) << "Invariant failed: assets available must be positive"; + result = false; + } + + if (afterVault.assetsAvailable > afterVault.assetsTotal) + { + JLOG(j.fatal()) << "Invariant failed: assets available must " + "not be greater than assets outstanding"; + result = false; + } + else if (afterVault.lossUnrealized > afterVault.assetsTotal - afterVault.assetsAvailable) + { + JLOG(j.fatal()) // + << "Invariant failed: loss unrealized must not exceed " + "the difference between assets outstanding and available"; + result = false; + } + + if (afterVault.assetsTotal < zero) + { + JLOG(j.fatal()) << "Invariant failed: assets outstanding must be positive"; + result = false; + } + + if (afterVault.assetsMaximum < zero) + { + JLOG(j.fatal()) << "Invariant failed: assets maximum must be positive"; + result = false; + } + + // Thanks to this check we can simply do `assert(!beforeVault_.empty()` when + // enforcing invariants on transaction types other than ttVAULT_CREATE + if (beforeVault_.empty() && txnType != ttVAULT_CREATE) + { + JLOG(j.fatal()) << // + "Invariant failed: vault created by a wrong transaction type"; + XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault creation invariant"); + return !enforce; // That's all we can do here + } + + if (!beforeVault_.empty() && afterVault.lossUnrealized != beforeVault_[0].lossUnrealized && + txnType != ttLOAN_MANAGE && txnType != ttLOAN_PAY) + { + JLOG(j.fatal()) << // + "Invariant failed: vault transaction must not change loss " + "unrealized"; + result = false; + } + + auto const beforeShares = [&]() -> std::optional { + if (beforeVault_.empty()) + return std::nullopt; + auto const& beforeVault = beforeVault_[0]; + + for (auto const& e : beforeMPTs_) + { + if (e.share.getMptID() == beforeVault.shareMPTID) + return std::move(e); + } + return std::nullopt; + }(); + + if (!beforeShares && + (tx.getTxnType() == ttVAULT_DEPOSIT || // + tx.getTxnType() == ttVAULT_WITHDRAW || // + tx.getTxnType() == ttVAULT_CLAWBACK)) + { + JLOG(j.fatal()) << "Invariant failed: vault operation succeeded " + "without updating shares"; + XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : shares noop invariant"); + return !enforce; // That's all we can do here + } + + auto const& vaultAsset = afterVault.asset; + auto const deltaAssets = [&](AccountID const& id) -> std::optional { + auto const get = // + [&](auto const& it, std::int8_t sign = 1) -> std::optional { + if (it == deltas_.end()) + return std::nullopt; + + return it->second * sign; + }; + + return std::visit( + [&](TIss const& issue) { + if constexpr (std::is_same_v) + { + if (isXRP(issue)) + return get(deltas_.find(keylet::account(id).key)); + return get( + deltas_.find(keylet::line(id, issue).key), id > issue.getIssuer() ? -1 : 1); + } + else if constexpr (std::is_same_v) + { + return get(deltas_.find(keylet::mptoken(issue.getMptID(), id).key)); + } + }, + vaultAsset.value()); + }; + auto const deltaAssetsTxAccount = [&]() -> std::optional { + auto ret = deltaAssets(tx[sfAccount]); + // Nothing returned or not XRP transaction + if (!ret.has_value() || !vaultAsset.native()) + return ret; + + // Delegated transaction; no need to compensate for fees + if (auto const delegate = tx[~sfDelegate]; + delegate.has_value() && *delegate != tx[sfAccount]) + return ret; + + *ret += fee.drops(); + if (*ret == zero) + return std::nullopt; + + return ret; + }; + auto const deltaShares = [&](AccountID const& id) -> std::optional { + auto const it = [&]() { + if (id == afterVault.pseudoId) + return deltas_.find(keylet::mptIssuance(afterVault.shareMPTID).key); + return deltas_.find(keylet::mptoken(afterVault.shareMPTID, id).key); + }(); + + return it != deltas_.end() ? std::optional(it->second) : std::nullopt; + }; + + auto const vaultHoldsNoAssets = [&](Vault const& vault) { + return vault.assetsAvailable == 0 && vault.assetsTotal == 0; + }; + + // Technically this does not need to be a lambda, but it's more + // convenient thanks to early "return false"; the not-so-nice + // alternatives are several layers of nested if/else or more complex + // (i.e. brittle) if statements. + result &= [&]() { + switch (txnType) + { + case ttVAULT_CREATE: { + bool result = true; + + if (!beforeVault_.empty()) + { + JLOG(j.fatal()) // + << "Invariant failed: create operation must not have " + "updated a vault"; + result = false; + } + + if (afterVault.assetsAvailable != zero || afterVault.assetsTotal != zero || + afterVault.lossUnrealized != zero || updatedShares->sharesTotal != 0) + { + JLOG(j.fatal()) // + << "Invariant failed: created vault must be empty"; + result = false; + } + + if (afterVault.pseudoId != updatedShares->share.getIssuer()) + { + JLOG(j.fatal()) // + << "Invariant failed: shares issuer and vault " + "pseudo-account must be the same"; + result = false; + } + + auto const sleSharesIssuer = + view.read(keylet::account(updatedShares->share.getIssuer())); + if (!sleSharesIssuer) + { + JLOG(j.fatal()) // + << "Invariant failed: shares issuer must exist"; + return false; + } + + if (!isPseudoAccount(sleSharesIssuer)) + { + JLOG(j.fatal()) // + << "Invariant failed: shares issuer must be a " + "pseudo-account"; + result = false; + } + + if (auto const vaultId = (*sleSharesIssuer)[~sfVaultID]; + !vaultId || *vaultId != afterVault.key) + { + JLOG(j.fatal()) // + << "Invariant failed: shares issuer pseudo-account " + "must point back to the vault"; + result = false; + } + + return result; + } + case ttVAULT_SET: { + bool result = true; + + XRPL_ASSERT( + !beforeVault_.empty(), "xrpl::ValidVault::finalize : set updated a vault"); + auto const& beforeVault = beforeVault_[0]; + + auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId); + if (vaultDeltaAssets) + { + JLOG(j.fatal()) << // + "Invariant failed: set must not change vault balance"; + result = false; + } + + if (beforeVault.assetsTotal != afterVault.assetsTotal) + { + JLOG(j.fatal()) << // + "Invariant failed: set must not change assets " + "outstanding"; + result = false; + } + + if (afterVault.assetsMaximum > zero && + afterVault.assetsTotal > afterVault.assetsMaximum) + { + JLOG(j.fatal()) << // + "Invariant failed: set assets outstanding must not " + "exceed assets maximum"; + result = false; + } + + if (beforeVault.assetsAvailable != afterVault.assetsAvailable) + { + JLOG(j.fatal()) << // + "Invariant failed: set must not change assets " + "available"; + result = false; + } + + if (beforeShares && updatedShares && + beforeShares->sharesTotal != updatedShares->sharesTotal) + { + JLOG(j.fatal()) << // + "Invariant failed: set must not change shares " + "outstanding"; + result = false; + } + + return result; + } + case ttVAULT_DEPOSIT: { + bool result = true; + + XRPL_ASSERT( + !beforeVault_.empty(), "xrpl::ValidVault::finalize : deposit updated a vault"); + auto const& beforeVault = beforeVault_[0]; + + auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId); + + if (!vaultDeltaAssets) + { + JLOG(j.fatal()) << // + "Invariant failed: deposit must change vault balance"; + return false; // That's all we can do + } + + if (*vaultDeltaAssets > tx[sfAmount]) + { + JLOG(j.fatal()) << // + "Invariant failed: deposit must not change vault " + "balance by more than deposited amount"; + result = false; + } + + if (*vaultDeltaAssets <= zero) + { + JLOG(j.fatal()) << // + "Invariant failed: deposit must increase vault balance"; + result = false; + } + + // Any payments (including deposits) made by the issuer + // do not change their balance, but create funds instead. + bool const issuerDeposit = [&]() -> bool { + if (vaultAsset.native()) + return false; + return tx[sfAccount] == vaultAsset.getIssuer(); + }(); + + if (!issuerDeposit) + { + auto const accountDeltaAssets = deltaAssetsTxAccount(); + if (!accountDeltaAssets) + { + JLOG(j.fatal()) << // + "Invariant failed: deposit must change depositor " + "balance"; + return false; + } + + if (*accountDeltaAssets >= zero) + { + JLOG(j.fatal()) << // + "Invariant failed: deposit must decrease depositor " + "balance"; + result = false; + } + + if (*accountDeltaAssets * -1 != *vaultDeltaAssets) + { + JLOG(j.fatal()) << // + "Invariant failed: deposit must change vault and " + "depositor balance by equal amount"; + result = false; + } + } + + if (afterVault.assetsMaximum > zero && + afterVault.assetsTotal > afterVault.assetsMaximum) + { + JLOG(j.fatal()) << // + "Invariant failed: deposit assets outstanding must not " + "exceed assets maximum"; + result = false; + } + + auto const accountDeltaShares = deltaShares(tx[sfAccount]); + if (!accountDeltaShares) + { + JLOG(j.fatal()) << // + "Invariant failed: deposit must change depositor " + "shares"; + return false; // That's all we can do + } + + if (*accountDeltaShares <= zero) + { + JLOG(j.fatal()) << // + "Invariant failed: deposit must increase depositor " + "shares"; + result = false; + } + + auto const vaultDeltaShares = deltaShares(afterVault.pseudoId); + if (!vaultDeltaShares || *vaultDeltaShares == zero) + { + JLOG(j.fatal()) << // + "Invariant failed: deposit must change vault shares"; + return false; // That's all we can do + } + + if (*vaultDeltaShares * -1 != *accountDeltaShares) + { + JLOG(j.fatal()) << // + "Invariant failed: deposit must change depositor and " + "vault shares by equal amount"; + result = false; + } + + if (beforeVault.assetsTotal + *vaultDeltaAssets != afterVault.assetsTotal) + { + JLOG(j.fatal()) << "Invariant failed: deposit and assets " + "outstanding must add up"; + result = false; + } + if (beforeVault.assetsAvailable + *vaultDeltaAssets != afterVault.assetsAvailable) + { + JLOG(j.fatal()) << "Invariant failed: deposit and assets " + "available must add up"; + result = false; + } + + return result; + } + case ttVAULT_WITHDRAW: { + bool result = true; + + XRPL_ASSERT( + !beforeVault_.empty(), + "xrpl::ValidVault::finalize : withdrawal updated a " + "vault"); + auto const& beforeVault = beforeVault_[0]; + + auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId); + + if (!vaultDeltaAssets) + { + JLOG(j.fatal()) << "Invariant failed: withdrawal must " + "change vault balance"; + return false; // That's all we can do + } + + if (*vaultDeltaAssets >= zero) + { + JLOG(j.fatal()) << "Invariant failed: withdrawal must " + "decrease vault balance"; + result = false; + } + + // Any payments (including withdrawal) going to the issuer + // do not change their balance, but destroy funds instead. + bool const issuerWithdrawal = [&]() -> bool { + if (vaultAsset.native()) + return false; + auto const destination = tx[~sfDestination].value_or(tx[sfAccount]); + return destination == vaultAsset.getIssuer(); + }(); + + if (!issuerWithdrawal) + { + auto const accountDeltaAssets = deltaAssetsTxAccount(); + auto const otherAccountDelta = [&]() -> std::optional { + if (auto const destination = tx[~sfDestination]; + destination && *destination != tx[sfAccount]) + return deltaAssets(*destination); + return std::nullopt; + }(); + + if (accountDeltaAssets.has_value() == otherAccountDelta.has_value()) + { + JLOG(j.fatal()) << // + "Invariant failed: withdrawal must change one " + "destination balance"; + return false; + } + + auto const destinationDelta = // + accountDeltaAssets ? *accountDeltaAssets : *otherAccountDelta; + + if (destinationDelta <= zero) + { + JLOG(j.fatal()) << // + "Invariant failed: withdrawal must increase " + "destination balance"; + result = false; + } + + if (*vaultDeltaAssets * -1 != destinationDelta) + { + JLOG(j.fatal()) << // + "Invariant failed: withdrawal must change vault " + "and destination balance by equal amount"; + result = false; + } + } + + auto const accountDeltaShares = deltaShares(tx[sfAccount]); + if (!accountDeltaShares) + { + JLOG(j.fatal()) << // + "Invariant failed: withdrawal must change depositor " + "shares"; + return false; + } + + if (*accountDeltaShares >= zero) + { + JLOG(j.fatal()) << // + "Invariant failed: withdrawal must decrease depositor " + "shares"; + result = false; + } + + auto const vaultDeltaShares = deltaShares(afterVault.pseudoId); + if (!vaultDeltaShares || *vaultDeltaShares == zero) + { + JLOG(j.fatal()) << // + "Invariant failed: withdrawal must change vault shares"; + return false; // That's all we can do + } + + if (*vaultDeltaShares * -1 != *accountDeltaShares) + { + JLOG(j.fatal()) << // + "Invariant failed: withdrawal must change depositor " + "and vault shares by equal amount"; + result = false; + } + + // Note, vaultBalance is negative (see check above) + if (beforeVault.assetsTotal + *vaultDeltaAssets != afterVault.assetsTotal) + { + JLOG(j.fatal()) << "Invariant failed: withdrawal and " + "assets outstanding must add up"; + result = false; + } + + if (beforeVault.assetsAvailable + *vaultDeltaAssets != afterVault.assetsAvailable) + { + JLOG(j.fatal()) << "Invariant failed: withdrawal and " + "assets available must add up"; + result = false; + } + + return result; + } + case ttVAULT_CLAWBACK: { + bool result = true; + + XRPL_ASSERT( + !beforeVault_.empty(), "xrpl::ValidVault::finalize : clawback updated a vault"); + auto const& beforeVault = beforeVault_[0]; + + if (vaultAsset.native() || vaultAsset.getIssuer() != tx[sfAccount]) + { + // The owner can use clawback to force-burn shares when the + // vault is empty but there are outstanding shares + if (!(beforeShares && beforeShares->sharesTotal > 0 && + vaultHoldsNoAssets(beforeVault) && beforeVault.owner == tx[sfAccount])) + { + JLOG(j.fatal()) << // + "Invariant failed: clawback may only be performed " + "by the asset issuer, or by the vault owner of an " + "empty vault"; + return false; // That's all we can do + } + } + + auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId); + if (vaultDeltaAssets) + { + if (*vaultDeltaAssets >= zero) + { + JLOG(j.fatal()) << // + "Invariant failed: clawback must decrease vault " + "balance"; + result = false; + } + + if (beforeVault.assetsTotal + *vaultDeltaAssets != afterVault.assetsTotal) + { + JLOG(j.fatal()) << // + "Invariant failed: clawback and assets outstanding " + "must add up"; + result = false; + } + + if (beforeVault.assetsAvailable + *vaultDeltaAssets != + afterVault.assetsAvailable) + { + JLOG(j.fatal()) << // + "Invariant failed: clawback and assets available " + "must add up"; + result = false; + } + } + else if (!vaultHoldsNoAssets(beforeVault)) + { + JLOG(j.fatal()) << // + "Invariant failed: clawback must change vault balance"; + return false; // That's all we can do + } + + auto const accountDeltaShares = deltaShares(tx[sfHolder]); + if (!accountDeltaShares) + { + JLOG(j.fatal()) << // + "Invariant failed: clawback must change holder shares"; + return false; // That's all we can do + } + + if (*accountDeltaShares >= zero) + { + JLOG(j.fatal()) << // + "Invariant failed: clawback must decrease holder " + "shares"; + result = false; + } + + auto const vaultDeltaShares = deltaShares(afterVault.pseudoId); + if (!vaultDeltaShares || *vaultDeltaShares == zero) + { + JLOG(j.fatal()) << // + "Invariant failed: clawback must change vault shares"; + return false; // That's all we can do + } + + if (*vaultDeltaShares * -1 != *accountDeltaShares) + { + JLOG(j.fatal()) << // + "Invariant failed: clawback must change holder and " + "vault shares by equal amount"; + result = false; + } + + return result; + } + + case ttLOAN_SET: + case ttLOAN_MANAGE: + case ttLOAN_PAY: { + // TBD + return true; + } + + default: + // LCOV_EXCL_START + UNREACHABLE("xrpl::ValidVault::finalize : unknown transaction type"); + return false; + // LCOV_EXCL_STOP + } + }(); + + if (!result) + { + // The comment at the top of this file starting with "assert(enforce)" + // explains this assert. + XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault invariants"); + return !enforce; + } + + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/PermissionedDomain/PermissionedDomainSet.cpp b/src/libxrpl/tx/transactors/PermissionedDomain/PermissionedDomainSet.cpp index 51ff4d8217..15bb79b239 100644 --- a/src/libxrpl/tx/transactors/PermissionedDomain/PermissionedDomainSet.cpp +++ b/src/libxrpl/tx/transactors/PermissionedDomain/PermissionedDomainSet.cpp @@ -1,10 +1,9 @@ +#include +// #include #include #include #include -#include - -#include namespace xrpl { From afc660a1b5764536cf40350714ee5849ac60a067 Mon Sep 17 00:00:00 2001 From: Alex Kremer Date: Mon, 2 Mar 2026 17:08:56 +0000 Subject: [PATCH 7/9] refactor: Fix clang-tidy `bugprone-empty-catch` check (#6419) This change fixes or suppresses instances detected by the `bugprone-empty-catch` clang-tidy check. --- .clang-tidy | 2 +- cspell.config.yaml | 2 ++ src/libxrpl/beast/insight/StatsDCollector.cpp | 2 +- src/libxrpl/nodestore/backend/NuDBFactory.cpp | 2 +- src/libxrpl/protocol/STAmount.cpp | 8 ++++---- src/libxrpl/protocol/STTx.cpp | 4 ++-- src/libxrpl/tx/transactors/XChainBridge.cpp | 2 +- src/test/app/Manifest_test.cpp | 4 ++-- src/test/core/SociDB_test.cpp | 4 ++-- src/test/jtx/impl/Env.cpp | 8 ++++---- src/test/jtx/impl/Oracle.cpp | 2 +- src/test/jtx/impl/WSClient.cpp | 1 + src/tests/libxrpl/basics/scope.cpp | 12 ++++++------ src/xrpld/app/ledger/detail/InboundLedgers.cpp | 2 +- src/xrpld/app/ledger/detail/LedgerMaster.cpp | 2 +- src/xrpld/app/ledger/detail/SkipListAcquire.cpp | 2 +- src/xrpld/app/main/GRPCServer.cpp | 2 +- src/xrpld/app/misc/detail/ValidatorSite.cpp | 4 ++-- src/xrpld/overlay/detail/ConnectAttempt.cpp | 2 +- src/xrpld/rpc/detail/RPCCall.cpp | 2 +- 20 files changed, 36 insertions(+), 33 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index 5f4187b008..5971b5dd14 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -10,6 +10,7 @@ Checks: "-*, bugprone-copy-constructor-init, bugprone-dangling-handle, bugprone-dynamic-static-initializers, + bugprone-empty-catch, bugprone-fold-init-type, bugprone-forward-declaration-namespace, bugprone-inaccurate-erase, @@ -83,7 +84,6 @@ Checks: "-*, # --- # checks that have some issues that need to be resolved: # -# bugprone-empty-catch, # bugprone-crtp-constructor-accessibility, # bugprone-inc-dec-in-conditions, # bugprone-reserved-identifier, diff --git a/cspell.config.yaml b/cspell.config.yaml index e2b20ac098..98b6be81e7 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -176,6 +176,8 @@ words: - nixfmt - nixos - nixpkgs + - NOLINT + - NOLINTNEXTLINE - nonxrp - noripple - nudb diff --git a/src/libxrpl/beast/insight/StatsDCollector.cpp b/src/libxrpl/beast/insight/StatsDCollector.cpp index 8462a00b3d..143bc51bd8 100644 --- a/src/libxrpl/beast/insight/StatsDCollector.cpp +++ b/src/libxrpl/beast/insight/StatsDCollector.cpp @@ -249,7 +249,7 @@ public: { m_timer.cancel(); } - catch (boost::system::system_error const&) + catch (boost::system::system_error const&) // NOLINT(bugprone-empty-catch) { // ignored } diff --git a/src/libxrpl/nodestore/backend/NuDBFactory.cpp b/src/libxrpl/nodestore/backend/NuDBFactory.cpp index 4d7e7be668..c79938bcf8 100644 --- a/src/libxrpl/nodestore/backend/NuDBFactory.cpp +++ b/src/libxrpl/nodestore/backend/NuDBFactory.cpp @@ -83,7 +83,7 @@ public: // close can throw and we don't want the destructor to throw. close(); } - catch (nudb::system_error const&) + catch (nudb::system_error const&) // NOLINT(bugprone-empty-catch) { // Don't allow exceptions to propagate out of destructors. // close() has already logged the error. diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 650cc4369d..9503da57a2 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -443,6 +443,7 @@ getRate(STAmount const& offerOut, STAmount const& offerIn) { if (offerOut == beast::zero) return 0; + try { STAmount r = divide(offerIn, offerOut, noIssue()); @@ -454,12 +455,11 @@ getRate(STAmount const& offerOut, STAmount const& offerIn) std::uint64_t ret = r.exponent() + 100; return (ret << (64 - 8)) | r.mantissa(); } - catch (std::exception const&) + catch (...) { + // overflow -- very bad offer + return 0; } - - // overflow -- very bad offer - return 0; } /** diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index 0c5e299702..098ca1a400 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -246,10 +246,10 @@ STTx::checkSign(Rules const& rules, STObject const& sigObject) const return signingPubKey.empty() ? checkMultiSign(rules, sigObject) : checkSingleSign(sigObject); } - catch (std::exception const&) + catch (...) { + return Unexpected("Internal signature check failure."); } - return Unexpected("Internal signature check failure."); } Expected diff --git a/src/libxrpl/tx/transactors/XChainBridge.cpp b/src/libxrpl/tx/transactors/XChainBridge.cpp index 30fc9f59e1..64daa6d1ee 100644 --- a/src/libxrpl/tx/transactors/XChainBridge.cpp +++ b/src/libxrpl/tx/transactors/XChainBridge.cpp @@ -1126,8 +1126,8 @@ toClaim(STTx const& tx) } catch (...) { + return std::nullopt; } - return std::nullopt; } template diff --git a/src/test/app/Manifest_test.cpp b/src/test/app/Manifest_test.cpp index a790584ac2..294d5210d9 100644 --- a/src/test/app/Manifest_test.cpp +++ b/src/test/app/Manifest_test.cpp @@ -71,7 +71,7 @@ public: { setupDatabaseDir(getDatabasePath()); } - catch (std::exception const&) + catch (std::exception const&) // NOLINT(bugprone-empty-catch) { } } @@ -81,7 +81,7 @@ public: { cleanupDatabaseDir(getDatabasePath()); } - catch (std::exception const&) + catch (std::exception const&) // NOLINT(bugprone-empty-catch) { } } diff --git a/src/test/core/SociDB_test.cpp b/src/test/core/SociDB_test.cpp index a06193ae86..66b368176d 100644 --- a/src/test/core/SociDB_test.cpp +++ b/src/test/core/SociDB_test.cpp @@ -58,7 +58,7 @@ public: { setupDatabaseDir(getDatabasePath()); } - catch (std::exception const&) + catch (std::exception const&) // NOLINT(bugprone-empty-catch) { } } @@ -68,7 +68,7 @@ public: { cleanupDatabaseDir(getDatabasePath()); } - catch (std::exception const&) + catch (std::exception const&) // NOLINT(bugprone-empty-catch) { } } diff --git a/src/test/jtx/impl/Env.cpp b/src/test/jtx/impl/Env.cpp index df86aaa2e4..4dfd2f2b38 100644 --- a/src/test/jtx/impl/Env.cpp +++ b/src/test/jtx/impl/Env.cpp @@ -587,10 +587,10 @@ Env::st(JTx const& jt) { return sterilize(STTx{std::move(*obj)}); } - catch (std::exception const&) + catch (...) { + return nullptr; } - return nullptr; } std::shared_ptr @@ -613,10 +613,10 @@ Env::ust(JTx const& jt) { return std::make_shared(std::move(*obj)); } - catch (std::exception const&) + catch (...) { + return nullptr; } - return nullptr; } Json::Value diff --git a/src/test/jtx/impl/Oracle.cpp b/src/test/jtx/impl/Oracle.cpp index c9d8c0ce27..302880c972 100644 --- a/src/test/jtx/impl/Oracle.cpp +++ b/src/test/jtx/impl/Oracle.cpp @@ -339,8 +339,8 @@ validDocumentID(AnyValue const& v) } catch (...) { + return false; } - return false; } } // namespace oracle diff --git a/src/test/jtx/impl/WSClient.cpp b/src/test/jtx/impl/WSClient.cpp index 2b92eb5ec3..84424be222 100644 --- a/src/test/jtx/impl/WSClient.cpp +++ b/src/test/jtx/impl/WSClient.cpp @@ -107,6 +107,7 @@ class WSClientImpl : public WSClient { stream_.cancel(); } + // NOLINTNEXTLINE(bugprone-empty-catch) catch (boost::system::system_error const&) { // ignored diff --git a/src/tests/libxrpl/basics/scope.cpp b/src/tests/libxrpl/basics/scope.cpp index 309a41ec04..8efa4a84b1 100644 --- a/src/tests/libxrpl/basics/scope.cpp +++ b/src/tests/libxrpl/basics/scope.cpp @@ -35,7 +35,7 @@ TEST(scope, scope_exit) scope_exit x{[&i]() { i = 5; }}; throw 1; } - catch (...) + catch (...) // NOLINT(bugprone-empty-catch) { } } @@ -47,7 +47,7 @@ TEST(scope, scope_exit) x.release(); throw 1; } - catch (...) + catch (...) // NOLINT(bugprone-empty-catch) { } } @@ -85,7 +85,7 @@ TEST(scope, scope_fail) scope_fail x{[&i]() { i = 5; }}; throw 1; } - catch (...) + catch (...) // NOLINT(bugprone-empty-catch) { } } @@ -97,7 +97,7 @@ TEST(scope, scope_fail) x.release(); throw 1; } - catch (...) + catch (...) // NOLINT(bugprone-empty-catch) { } } @@ -135,7 +135,7 @@ TEST(scope, scope_success) scope_success x{[&i]() { i = 5; }}; throw 1; } - catch (...) + catch (...) // NOLINT(bugprone-empty-catch) { } } @@ -147,7 +147,7 @@ TEST(scope, scope_success) x.release(); throw 1; } - catch (...) + catch (...) // NOLINT(bugprone-empty-catch) { } } diff --git a/src/xrpld/app/ledger/detail/InboundLedgers.cpp b/src/xrpld/app/ledger/detail/InboundLedgers.cpp index a8ae530bde..e17437d64f 100644 --- a/src/xrpld/app/ledger/detail/InboundLedgers.cpp +++ b/src/xrpld/app/ledger/detail/InboundLedgers.cpp @@ -241,7 +241,7 @@ public: newNode->getHash().as_uint256(), std::make_shared(s.begin(), s.end())); } } - catch (std::exception const&) + catch (std::exception const&) // NOLINT(bugprone-empty-catch) { } } diff --git a/src/xrpld/app/ledger/detail/LedgerMaster.cpp b/src/xrpld/app/ledger/detail/LedgerMaster.cpp index 8072b619e1..64bdf04df1 100644 --- a/src/xrpld/app/ledger/detail/LedgerMaster.cpp +++ b/src/xrpld/app/ledger/detail/LedgerMaster.cpp @@ -1637,7 +1637,7 @@ LedgerMaster::getLedgerBySeq(std::uint32_t index) if (hash) return mLedgerHistory.getLedgerByHash(*hash); } - catch (std::exception const&) + catch (std::exception const&) // NOLINT(bugprone-empty-catch) { // Missing nodes are already handled } diff --git a/src/xrpld/app/ledger/detail/SkipListAcquire.cpp b/src/xrpld/app/ledger/detail/SkipListAcquire.cpp index 0fb1239c49..2191ef965a 100644 --- a/src/xrpld/app/ledger/detail/SkipListAcquire.cpp +++ b/src/xrpld/app/ledger/detail/SkipListAcquire.cpp @@ -127,7 +127,7 @@ SkipListAcquire::processData( return; } } - catch (...) + catch (...) // NOLINT(bugprone-empty-catch) { } diff --git a/src/xrpld/app/main/GRPCServer.cpp b/src/xrpld/app/main/GRPCServer.cpp index ced252cb71..c6b5c91e14 100644 --- a/src/xrpld/app/main/GRPCServer.cpp +++ b/src/xrpld/app/main/GRPCServer.cpp @@ -29,7 +29,7 @@ getEndpoint(std::string const& peer) if (endpoint) return beast::IP::to_asio_endpoint(endpoint.value()); } - catch (std::exception const&) + catch (std::exception const&) // NOLINT(bugprone-empty-catch) { } return {}; diff --git a/src/xrpld/app/misc/detail/ValidatorSite.cpp b/src/xrpld/app/misc/detail/ValidatorSite.cpp index fb68bf5ef4..c4077a1b8b 100644 --- a/src/xrpld/app/misc/detail/ValidatorSite.cpp +++ b/src/xrpld/app/misc/detail/ValidatorSite.cpp @@ -177,7 +177,7 @@ ValidatorSite::stop() { timer_.cancel(); } - catch (boost::system::system_error const&) + catch (boost::system::system_error const&) // NOLINT(bugprone-empty-catch) { } stopping_ = false; @@ -222,7 +222,7 @@ ValidatorSite::makeRequest( { timer_.cancel_one(); } - catch (boost::system::system_error const&) + catch (boost::system::system_error const&) // NOLINT(bugprone-empty-catch) { } }; diff --git a/src/xrpld/overlay/detail/ConnectAttempt.cpp b/src/xrpld/overlay/detail/ConnectAttempt.cpp index c9361a2a5d..ac0743e936 100644 --- a/src/xrpld/overlay/detail/ConnectAttempt.cpp +++ b/src/xrpld/overlay/detail/ConnectAttempt.cpp @@ -252,7 +252,7 @@ ConnectAttempt::cancelTimer() timer_.cancel(); stepTimer_.cancel(); } - catch (boost::system::system_error const&) + catch (boost::system::system_error const&) // NOLINT(bugprone-empty-catch) { // ignored } diff --git a/src/xrpld/rpc/detail/RPCCall.cpp b/src/xrpld/rpc/detail/RPCCall.cpp index 7b65daa839..134cbb34f8 100644 --- a/src/xrpld/rpc/detail/RPCCall.cpp +++ b/src/xrpld/rpc/detail/RPCCall.cpp @@ -1479,7 +1479,7 @@ rpcClient( setup = setup_ServerHandler( config, beast::logstream{logs.journal("HTTPClient").warn()}); } - catch (std::exception const&) + catch (std::exception const&) // NOLINT(bugprone-empty-catch) { // ignore any exceptions, so the command // line client works without a config file From 5300e656864e5cbdd94ab0615d5aacd1e705f202 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Tue, 3 Mar 2026 13:46:55 +0000 Subject: [PATCH 8/9] tests: Improve stability of Subscribe tests (#6420) The `Subscribe` tests were flaky, because each test performs some operations (e.g. sends transactions) and waits for messages to appear in subscription with a 100ms timeout. If tests are slow (e.g. compiled in debug mode or a slow machine) then some of them could fail. This change adds an attempt to synchronize the background Env's thread and the test's thread by ensuring that all the scheduled operations are started before the test's thread starts to wait for a websocket message. This is done by limiting I/O threads of the app inside Env to 1 and adding a synchronization barrier after closing the ledger. --- src/test/jtx/Env.h | 43 +++++++++++ src/test/jtx/envconfig.h | 2 + src/test/jtx/impl/envconfig.cpp | 6 ++ src/test/rpc/Subscribe_test.cpp | 113 +++++++++++++++-------------- src/xrpld/app/main/Application.cpp | 6 ++ src/xrpld/app/main/Application.h | 4 + src/xrpld/app/main/BasicApp.h | 6 ++ 7 files changed, 127 insertions(+), 53 deletions(-) diff --git a/src/test/jtx/Env.h b/src/test/jtx/Env.h index 9caf257aa1..2ac0ca7435 100644 --- a/src/test/jtx/Env.h +++ b/src/test/jtx/Env.h @@ -31,6 +31,7 @@ #include #include +#include #include #include #include @@ -393,6 +394,48 @@ public: return close(std::chrono::seconds(5)); } + /** Close and advance the ledger, then synchronize with the server's + io_context to ensure all async operations initiated by the close have + been started. + + This function performs the same ledger close as close(), but additionally + ensures that all tasks posted to the server's io_context (such as + WebSocket subscription message sends) have been initiated before returning. + + What it guarantees: + - All async operations posted before syncClose() have been STARTED + - For WebSocket sends: async_write_some() has been called + - The actual I/O completion may still be pending (async) + + What it does NOT guarantee: + - Async operations have COMPLETED + - WebSocket messages have been received by clients + - However, for localhost connections, the remaining latency is typically + microseconds, making tests reliable + + Use this instead of close() when: + - Test code immediately checks for subscription messages + - Race conditions between test and worker threads must be avoided + - Deterministic test behavior is required + + @param timeout Maximum time to wait for the barrier task to execute + @return true if close succeeded and barrier executed within timeout, + false otherwise + */ + [[nodiscard]] bool + syncClose(std::chrono::steady_clock::duration timeout = std::chrono::seconds{1}) + { + XRPL_ASSERT( + app().getNumberOfThreads() == 1, + "syncClose() is only useful on an application with a single thread"); + auto const result = close(); + auto serverBarrier = std::make_shared>(); + auto future = serverBarrier->get_future(); + boost::asio::post(app().getIOContext(), [serverBarrier]() { serverBarrier->set_value(); }); + auto const status = future.wait_for(timeout); + return result && status == std::future_status::ready; + } + /** Turn on JSON tracing. With no arguments, trace all */ diff --git a/src/test/jtx/envconfig.h b/src/test/jtx/envconfig.h index f2f67f935b..e4a1975e74 100644 --- a/src/test/jtx/envconfig.h +++ b/src/test/jtx/envconfig.h @@ -73,6 +73,8 @@ std::unique_ptr admin_localnet(std::unique_ptr); std::unique_ptr secure_gateway_localnet(std::unique_ptr); +std::unique_ptr single_thread_io(std::unique_ptr); + /// @brief adjust configuration with params needed to be a validator /// /// this is intended for use with envconfig, as in diff --git a/src/test/jtx/impl/envconfig.cpp b/src/test/jtx/impl/envconfig.cpp index 31034f3b63..e31e687c3d 100644 --- a/src/test/jtx/impl/envconfig.cpp +++ b/src/test/jtx/impl/envconfig.cpp @@ -87,6 +87,12 @@ secure_gateway_localnet(std::unique_ptr cfg) (*cfg)[PORT_WS].set("secure_gateway", "127.0.0.0/8"); return cfg; } +std::unique_ptr +single_thread_io(std::unique_ptr cfg) +{ + cfg->IO_WORKERS = 1; + return cfg; +} auto constexpr defaultseed = "shUwVw52ofnCUX5m7kPTKzJdr4HEH"; diff --git a/src/test/rpc/Subscribe_test.cpp b/src/test/rpc/Subscribe_test.cpp index d83711324d..414bceefd7 100644 --- a/src/test/rpc/Subscribe_test.cpp +++ b/src/test/rpc/Subscribe_test.cpp @@ -26,7 +26,7 @@ public: { using namespace std::chrono_literals; using namespace jtx; - Env env(*this); + Env env{*this, single_thread_io(envconfig())}; auto wsc = makeWSClient(env.app().config()); Json::Value stream; @@ -92,7 +92,7 @@ public: { using namespace std::chrono_literals; using namespace jtx; - Env env(*this); + Env env{*this, single_thread_io(envconfig())}; auto wsc = makeWSClient(env.app().config()); Json::Value stream; @@ -114,7 +114,7 @@ public: { // Accept a ledger - env.close(); + BEAST_EXPECT(env.syncClose()); // Check stream update BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) { @@ -125,7 +125,7 @@ public: { // Accept another ledger - env.close(); + BEAST_EXPECT(env.syncClose()); // Check stream update BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) { @@ -150,7 +150,7 @@ public: { using namespace std::chrono_literals; using namespace jtx; - Env env(*this); + Env env(*this, single_thread_io(envconfig())); auto baseFee = env.current()->fees().base.drops(); auto wsc = makeWSClient(env.app().config()); Json::Value stream; @@ -171,7 +171,7 @@ public: { env.fund(XRP(10000), "alice"); - env.close(); + BEAST_EXPECT(env.syncClose()); // Check stream update for payment transaction BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) { @@ -195,7 +195,7 @@ public: })); env.fund(XRP(10000), "bob"); - env.close(); + BEAST_EXPECT(env.syncClose()); // Check stream update for payment transaction BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) { @@ -249,12 +249,12 @@ public: { // Transaction that does not affect stream env.fund(XRP(10000), "carol"); - env.close(); + BEAST_EXPECT(env.syncClose()); BEAST_EXPECT(!wsc->getMsg(10ms)); // Transactions concerning alice env.trust(Account("bob")["USD"](100), "alice"); - env.close(); + BEAST_EXPECT(env.syncClose()); // Check stream updates BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) { @@ -288,6 +288,7 @@ public: using namespace jtx; Env env(*this, envconfig([](std::unique_ptr cfg) { cfg->FEES.reference_fee = 10; + cfg = single_thread_io(std::move(cfg)); return cfg; })); auto wsc = makeWSClient(env.app().config()); @@ -310,7 +311,7 @@ public: { env.fund(XRP(10000), "alice"); - env.close(); + BEAST_EXPECT(env.syncClose()); // Check stream update for payment transaction BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) { @@ -360,7 +361,7 @@ public: testManifests() { using namespace jtx; - Env env(*this); + Env env(*this, single_thread_io(envconfig())); auto wsc = makeWSClient(env.app().config()); Json::Value stream; @@ -394,7 +395,7 @@ public: { using namespace jtx; - Env env{*this, envconfig(validator, ""), features}; + Env env{*this, single_thread_io(envconfig(validator, "")), features}; auto& cfg = env.app().config(); if (!BEAST_EXPECT(cfg.section(SECTION_VALIDATION_SEED).empty())) return; @@ -483,7 +484,7 @@ public: // at least one flag ledger. while (env.closed()->header().seq < 300) { - env.close(); + BEAST_EXPECT(env.syncClose()); using namespace std::chrono_literals; BEAST_EXPECT(wsc->findMsg(5s, validValidationFields)); } @@ -505,7 +506,7 @@ public: { using namespace jtx; testcase("Subscribe by url"); - Env env{*this}; + Env env{*this, single_thread_io(envconfig())}; Json::Value jv; jv[jss::url] = "http://localhost/events"; @@ -536,7 +537,7 @@ public: auto const method = subscribe ? "subscribe" : "unsubscribe"; testcase << "Error cases for " << method; - Env env{*this}; + Env env{*this, single_thread_io(envconfig())}; auto wsc = makeWSClient(env.app().config()); { @@ -572,7 +573,7 @@ public: } { - Env env_nonadmin{*this, no_admin(envconfig())}; + Env env_nonadmin{*this, single_thread_io(no_admin(envconfig()))}; Json::Value jv; jv[jss::url] = "no-url"; auto jr = env_nonadmin.rpc("json", method, to_string(jv))[jss::result]; @@ -834,12 +835,13 @@ public: * send payments between the two accounts a and b, * and close ledgersToClose ledgers */ - auto sendPayments = [](Env& env, - Account const& a, - Account const& b, - int newTxns, - std::uint32_t ledgersToClose, - int numXRP = 10) { + auto sendPayments = [this]( + Env& env, + Account const& a, + Account const& b, + int newTxns, + std::uint32_t ledgersToClose, + int numXRP = 10) { env.memoize(a); env.memoize(b); for (int i = 0; i < newTxns; ++i) @@ -852,7 +854,7 @@ public: jtx::sig(jtx::autofill)); } for (int i = 0; i < ledgersToClose; ++i) - env.close(); + BEAST_EXPECT(env.syncClose()); return newTxns; }; @@ -945,7 +947,7 @@ public: * * also test subscribe to the account before it is created */ - Env env(*this); + Env env(*this, single_thread_io(envconfig())); auto wscTxHistory = makeWSClient(env.app().config()); Json::Value request; request[jss::account_history_tx_stream] = Json::objectValue; @@ -988,7 +990,7 @@ public: * subscribe genesis account tx history without txns * subscribe to bob's account after it is created */ - Env env(*this); + Env env(*this, single_thread_io(envconfig())); auto wscTxHistory = makeWSClient(env.app().config()); Json::Value request; request[jss::account_history_tx_stream] = Json::objectValue; @@ -998,6 +1000,7 @@ public: if (!BEAST_EXPECT(goodSubRPC(jv))) return; IdxHashVec genesisFullHistoryVec; + BEAST_EXPECT(env.syncClose()); if (!BEAST_EXPECT(!getTxHash(*wscTxHistory, genesisFullHistoryVec, 1).first)) return; @@ -1016,6 +1019,7 @@ public: if (!BEAST_EXPECT(goodSubRPC(jv))) return; IdxHashVec bobFullHistoryVec; + BEAST_EXPECT(env.syncClose()); r = getTxHash(*wscTxHistory, bobFullHistoryVec, 1); if (!BEAST_EXPECT(r.first && r.second)) return; @@ -1050,6 +1054,7 @@ public: "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; jv = wscTxHistory->invoke("subscribe", request); genesisFullHistoryVec.clear(); + BEAST_EXPECT(env.syncClose()); BEAST_EXPECT(getTxHash(*wscTxHistory, genesisFullHistoryVec, 31).second); jv = wscTxHistory->invoke("unsubscribe", request); @@ -1062,13 +1067,13 @@ public: * subscribe account and subscribe account tx history * and compare txns streamed */ - Env env(*this); + Env env(*this, single_thread_io(envconfig())); auto wscAccount = makeWSClient(env.app().config()); auto wscTxHistory = makeWSClient(env.app().config()); std::array accounts = {alice, bob}; env.fund(XRP(222222), accounts); - env.close(); + BEAST_EXPECT(env.syncClose()); // subscribe account Json::Value stream = Json::objectValue; @@ -1131,18 +1136,18 @@ public: * alice issues USD to carol * mix USD and XRP payments */ - Env env(*this); + Env env(*this, single_thread_io(envconfig())); auto const USD_a = alice["USD"]; std::array accounts = {alice, carol}; env.fund(XRP(333333), accounts); env.trust(USD_a(20000), carol); - env.close(); + BEAST_EXPECT(env.syncClose()); auto mixedPayments = [&]() -> int { sendPayments(env, alice, carol, 1, 0); env(pay(alice, carol, USD_a(100))); - env.close(); + BEAST_EXPECT(env.syncClose()); return 2; }; @@ -1152,6 +1157,7 @@ public: request[jss::account_history_tx_stream][jss::account] = carol.human(); auto ws = makeWSClient(env.app().config()); auto jv = ws->invoke("subscribe", request); + BEAST_EXPECT(env.syncClose()); { // take out existing txns from the stream IdxHashVec tempVec; @@ -1169,10 +1175,10 @@ public: /* * long transaction history */ - Env env(*this); + Env env(*this, single_thread_io(envconfig())); std::array accounts = {alice, carol}; env.fund(XRP(444444), accounts); - env.close(); + BEAST_EXPECT(env.syncClose()); // many payments, and close lots of ledgers auto oneRound = [&](int numPayments) { @@ -1185,6 +1191,7 @@ public: request[jss::account_history_tx_stream][jss::account] = carol.human(); auto wscLong = makeWSClient(env.app().config()); auto jv = wscLong->invoke("subscribe", request); + BEAST_EXPECT(env.syncClose()); { // take out existing txns from the stream IdxHashVec tempVec; @@ -1222,7 +1229,7 @@ public: jtx::testable_amendments() | featurePermissionedDomains | featureCredentials | featurePermissionedDEX}; - Env env(*this, all); + Env env(*this, single_thread_io(envconfig()), all); PermissionedDEX permDex(env); auto const alice = permDex.alice; auto const bob = permDex.bob; @@ -1241,10 +1248,10 @@ public: if (!BEAST_EXPECT(jv[jss::status] == "success")) return; env(offer(alice, XRP(10), USD(10)), domain(domainID), txflags(tfHybrid)); - env.close(); + BEAST_EXPECT(env.syncClose()); env(pay(bob, carol, USD(5)), path(~USD), sendmax(XRP(5)), domain(domainID)); - env.close(); + BEAST_EXPECT(env.syncClose()); BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) { if (jv[jss::changes].size() != 1) @@ -1284,9 +1291,9 @@ public: Account const bob{"bob"}; Account const broker{"broker"}; - Env env{*this, features}; + Env env{*this, single_thread_io(envconfig()), features}; env.fund(XRP(10000), alice, bob, broker); - env.close(); + BEAST_EXPECT(env.syncClose()); auto wsc = test::makeWSClient(env.app().config()); Json::Value stream; @@ -1350,12 +1357,12 @@ public: // Verify the NFTokenIDs are correct in the NFTokenMint tx meta uint256 const nftId1{token::getNextID(env, alice, 0u, tfTransferable)}; env(token::mint(alice, 0u), txflags(tfTransferable)); - env.close(); + BEAST_EXPECT(env.syncClose()); verifyNFTokenID(nftId1); uint256 const nftId2{token::getNextID(env, alice, 0u, tfTransferable)}; env(token::mint(alice, 0u), txflags(tfTransferable)); - env.close(); + BEAST_EXPECT(env.syncClose()); verifyNFTokenID(nftId2); // Alice creates one sell offer for each NFT @@ -1363,32 +1370,32 @@ public: // meta uint256 const aliceOfferIndex1 = keylet::nftoffer(alice, env.seq(alice)).key; env(token::createOffer(alice, nftId1, drops(1)), txflags(tfSellNFToken)); - env.close(); + BEAST_EXPECT(env.syncClose()); verifyNFTokenOfferID(aliceOfferIndex1); uint256 const aliceOfferIndex2 = keylet::nftoffer(alice, env.seq(alice)).key; env(token::createOffer(alice, nftId2, drops(1)), txflags(tfSellNFToken)); - env.close(); + BEAST_EXPECT(env.syncClose()); verifyNFTokenOfferID(aliceOfferIndex2); // Alice cancels two offers she created // Verify the NFTokenIDs are correct in the NFTokenCancelOffer tx // meta env(token::cancelOffer(alice, {aliceOfferIndex1, aliceOfferIndex2})); - env.close(); + BEAST_EXPECT(env.syncClose()); verifyNFTokenIDsInCancelOffer({nftId1, nftId2}); // Bobs creates a buy offer for nftId1 // Verify the offer id is correct in the NFTokenCreateOffer tx meta auto const bobBuyOfferIndex = keylet::nftoffer(bob, env.seq(bob)).key; env(token::createOffer(bob, nftId1, drops(1)), token::owner(alice)); - env.close(); + BEAST_EXPECT(env.syncClose()); verifyNFTokenOfferID(bobBuyOfferIndex); // Alice accepts bob's buy offer // Verify the NFTokenID is correct in the NFTokenAcceptOffer tx meta env(token::acceptBuyOffer(alice, bobBuyOfferIndex)); - env.close(); + BEAST_EXPECT(env.syncClose()); verifyNFTokenID(nftId1); } @@ -1397,7 +1404,7 @@ public: // Alice mints a NFT uint256 const nftId{token::getNextID(env, alice, 0u, tfTransferable)}; env(token::mint(alice, 0u), txflags(tfTransferable)); - env.close(); + BEAST_EXPECT(env.syncClose()); verifyNFTokenID(nftId); // Alice creates sell offer and set broker as destination @@ -1405,18 +1412,18 @@ public: env(token::createOffer(alice, nftId, drops(1)), token::destination(broker), txflags(tfSellNFToken)); - env.close(); + BEAST_EXPECT(env.syncClose()); verifyNFTokenOfferID(offerAliceToBroker); // Bob creates buy offer uint256 const offerBobToBroker = keylet::nftoffer(bob, env.seq(bob)).key; env(token::createOffer(bob, nftId, drops(1)), token::owner(alice)); - env.close(); + BEAST_EXPECT(env.syncClose()); verifyNFTokenOfferID(offerBobToBroker); // Check NFTokenID meta for NFTokenAcceptOffer in brokered mode env(token::brokerOffers(broker, offerBobToBroker, offerAliceToBroker)); - env.close(); + BEAST_EXPECT(env.syncClose()); verifyNFTokenID(nftId); } @@ -1426,24 +1433,24 @@ public: // Alice mints a NFT uint256 const nftId{token::getNextID(env, alice, 0u, tfTransferable)}; env(token::mint(alice, 0u), txflags(tfTransferable)); - env.close(); + BEAST_EXPECT(env.syncClose()); verifyNFTokenID(nftId); // Alice creates 2 sell offers for the same NFT uint256 const aliceOfferIndex1 = keylet::nftoffer(alice, env.seq(alice)).key; env(token::createOffer(alice, nftId, drops(1)), txflags(tfSellNFToken)); - env.close(); + BEAST_EXPECT(env.syncClose()); verifyNFTokenOfferID(aliceOfferIndex1); uint256 const aliceOfferIndex2 = keylet::nftoffer(alice, env.seq(alice)).key; env(token::createOffer(alice, nftId, drops(1)), txflags(tfSellNFToken)); - env.close(); + BEAST_EXPECT(env.syncClose()); verifyNFTokenOfferID(aliceOfferIndex2); // Make sure the metadata only has 1 nft id, since both offers are // for the same nft env(token::cancelOffer(alice, {aliceOfferIndex1, aliceOfferIndex2})); - env.close(); + BEAST_EXPECT(env.syncClose()); verifyNFTokenIDsInCancelOffer({nftId}); } @@ -1451,7 +1458,7 @@ public: { uint256 const aliceMintWithOfferIndex1 = keylet::nftoffer(alice, env.seq(alice)).key; env(token::mint(alice), token::amount(XRP(0))); - env.close(); + BEAST_EXPECT(env.syncClose()); verifyNFTokenOfferID(aliceMintWithOfferIndex1); } } diff --git a/src/xrpld/app/main/Application.cpp b/src/xrpld/app/main/Application.cpp index 1162bc497a..3e3d87dcd5 100644 --- a/src/xrpld/app/main/Application.cpp +++ b/src/xrpld/app/main/Application.cpp @@ -1072,6 +1072,12 @@ public: return trapTxID_; } + size_t + getNumberOfThreads() const override + { + return get_number_of_threads(); + } + private: // For a newly-started validator, this is the greatest persisted ledger // and new validations must be greater than this. diff --git a/src/xrpld/app/main/Application.h b/src/xrpld/app/main/Application.h index 433992bcda..0000ae010b 100644 --- a/src/xrpld/app/main/Application.h +++ b/src/xrpld/app/main/Application.h @@ -157,6 +157,10 @@ public: * than the last ledger it persisted. */ virtual LedgerIndex getMaxDisallowedLedger() = 0; + + /** Returns the number of io_context (I/O worker) threads used by the application. */ + virtual size_t + getNumberOfThreads() const = 0; }; std::unique_ptr diff --git a/src/xrpld/app/main/BasicApp.h b/src/xrpld/app/main/BasicApp.h index 278c255af3..19f07d1e5b 100644 --- a/src/xrpld/app/main/BasicApp.h +++ b/src/xrpld/app/main/BasicApp.h @@ -23,4 +23,10 @@ public: { return io_context_; } + + size_t + get_number_of_threads() const + { + return threads_.size(); + } }; From 0abd76278139e72b0f34c3f12d8d8332d23046ac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:17:08 +0000 Subject: [PATCH 9/9] ci: [DEPENDABOT] bump actions/upload-artifact from 6.0.0 to 7.0.0 (#6450) --- .github/workflows/reusable-build-test-config.yml | 2 +- .github/workflows/reusable-clang-tidy-files.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/reusable-build-test-config.yml b/.github/workflows/reusable-build-test-config.yml index dabcc737f8..75fe546b18 100644 --- a/.github/workflows/reusable-build-test-config.yml +++ b/.github/workflows/reusable-build-test-config.yml @@ -177,7 +177,7 @@ jobs: - name: Upload the binary (Linux) if: ${{ github.repository_owner == 'XRPLF' && runner.os == 'Linux' }} - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: xrpld-${{ inputs.config_name }} path: ${{ env.BUILD_DIR }}/xrpld diff --git a/.github/workflows/reusable-clang-tidy-files.yml b/.github/workflows/reusable-clang-tidy-files.yml index d36dea747c..129726ec8f 100644 --- a/.github/workflows/reusable-clang-tidy-files.yml +++ b/.github/workflows/reusable-clang-tidy-files.yml @@ -84,7 +84,7 @@ jobs: - name: Upload clang-tidy output if: steps.run_clang_tidy.outcome != 'success' - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: clang-tidy-results path: clang-tidy-output.txt