diff --git a/.github/scripts/strategy-matrix/generate.py b/.github/scripts/strategy-matrix/generate.py index fd05895b0e..025d553b5e 100755 --- a/.github/scripts/strategy-matrix/generate.py +++ b/.github/scripts/strategy-matrix/generate.py @@ -74,14 +74,14 @@ def generate_strategy_matrix(all: bool, config: Config) -> list: continue # RHEL: - # - 9.4 using GCC 12: Debug and Unity on linux/amd64. - # - 9.6 using Clang: Release and no Unity on linux/amd64. + # - 9 using GCC 12: Debug and Unity on linux/amd64. + # - 10 using Clang: Release and no Unity on linux/amd64. if os['distro_name'] == 'rhel': skip = True - if os['distro_version'] == '9.4': + if os['distro_version'] == '9': if f'{os['compiler_name']}-{os['compiler_version']}' == 'gcc-12' and build_type == 'Debug' and '-Dunity=ON' in cmake_args and architecture['platform'] == 'linux/amd64': skip = False - elif os['distro_version'] == '9.6': + elif os['distro_version'] == '10': if f'{os['compiler_name']}-{os['compiler_version']}' == 'clang-any' and build_type == 'Release' and '-Dunity=OFF' in cmake_args and architecture['platform'] == 'linux/amd64': skip = False if skip: diff --git a/.github/scripts/strategy-matrix/linux.json b/.github/scripts/strategy-matrix/linux.json index 44eaebd074..08313daf0a 100644 --- a/.github/scripts/strategy-matrix/linux.json +++ b/.github/scripts/strategy-matrix/linux.json @@ -14,139 +14,155 @@ "distro_name": "debian", "distro_version": "bookworm", "compiler_name": "gcc", - "compiler_version": "12" + "compiler_version": "12", + "image_sha": "6f723eb" }, { "distro_name": "debian", "distro_version": "bookworm", "compiler_name": "gcc", - "compiler_version": "13" + "compiler_version": "13", + "image_sha": "6f723eb" }, { "distro_name": "debian", "distro_version": "bookworm", "compiler_name": "gcc", - "compiler_version": "14" + "compiler_version": "14", + "image_sha": "6f723eb" }, { "distro_name": "debian", "distro_version": "bookworm", "compiler_name": "gcc", - "compiler_version": "15" + "compiler_version": "15", + "image_sha": "6f723eb" }, { "distro_name": "debian", "distro_version": "bookworm", "compiler_name": "clang", - "compiler_version": "16" + "compiler_version": "16", + "image_sha": "6f723eb" }, { "distro_name": "debian", "distro_version": "bookworm", "compiler_name": "clang", - "compiler_version": "17" + "compiler_version": "17", + "image_sha": "6f723eb" }, { "distro_name": "debian", "distro_version": "bookworm", "compiler_name": "clang", - "compiler_version": "18" + "compiler_version": "18", + "image_sha": "6f723eb" }, { "distro_name": "debian", "distro_version": "bookworm", "compiler_name": "clang", - "compiler_version": "19" + "compiler_version": "19", + "image_sha": "6f723eb" }, { "distro_name": "debian", "distro_version": "bookworm", "compiler_name": "clang", - "compiler_version": "20" + "compiler_version": "20", + "image_sha": "6f723eb" }, { "distro_name": "rhel", - "distro_version": "9.4", + "distro_version": "9", "compiler_name": "gcc", - "compiler_version": "12" + "compiler_version": "12", + "image_sha": "0ab1e4c" }, { "distro_name": "rhel", - "distro_version": "9.4", + "distro_version": "9", "compiler_name": "gcc", - "compiler_version": "13" + "compiler_version": "13", + "image_sha": "0ab1e4c" }, { "distro_name": "rhel", - "distro_version": "9.4", + "distro_version": "9", "compiler_name": "gcc", - "compiler_version": "14" + "compiler_version": "14", + "image_sha": "0ab1e4c" }, { "distro_name": "rhel", - "distro_version": "9.6", - "compiler_name": "gcc", - "compiler_version": "13" - }, - { - "distro_name": "rhel", - "distro_version": "9.6", - "compiler_name": "gcc", - "compiler_version": "14" - }, - { - "distro_name": "rhel", - "distro_version": "9.4", + "distro_version": "9", "compiler_name": "clang", - "compiler_version": "any" + "compiler_version": "any", + "image_sha": "0ab1e4c" }, { "distro_name": "rhel", - "distro_version": "9.6", + "distro_version": "10", + "compiler_name": "gcc", + "compiler_version": "14", + "image_sha": "0ab1e4c" + }, + { + "distro_name": "rhel", + "distro_version": "10", "compiler_name": "clang", - "compiler_version": "any" + "compiler_version": "any", + "image_sha": "0ab1e4c" }, { "distro_name": "ubuntu", "distro_version": "jammy", "compiler_name": "gcc", - "compiler_version": "12" + "compiler_version": "12", + "image_sha": "6f723eb" }, { "distro_name": "ubuntu", "distro_version": "noble", "compiler_name": "gcc", - "compiler_version": "13" + "compiler_version": "13", + "image_sha": "6f723eb" }, { "distro_name": "ubuntu", "distro_version": "noble", "compiler_name": "gcc", - "compiler_version": "14" + "compiler_version": "14", + "image_sha": "6f723eb" }, { "distro_name": "ubuntu", "distro_version": "noble", "compiler_name": "clang", - "compiler_version": "16" + "compiler_version": "16", + "image_sha": "6f723eb" }, { "distro_name": "ubuntu", "distro_version": "noble", "compiler_name": "clang", - "compiler_version": "17" + "compiler_version": "17", + "image_sha": "6f723eb" }, { "distro_name": "ubuntu", "distro_version": "noble", "compiler_name": "clang", - "compiler_version": "18" + "compiler_version": "18", + "image_sha": "6f723eb" }, { "distro_name": "ubuntu", "distro_version": "noble", "compiler_name": "clang", - "compiler_version": "19" + "compiler_version": "19", + "image_sha": "6f723eb" } ], "build_type": ["Debug", "Release"], diff --git a/.github/scripts/strategy-matrix/macos.json b/.github/scripts/strategy-matrix/macos.json index de37639ddd..14b6089620 100644 --- a/.github/scripts/strategy-matrix/macos.json +++ b/.github/scripts/strategy-matrix/macos.json @@ -10,7 +10,8 @@ "distro_name": "macos", "distro_version": "", "compiler_name": "", - "compiler_version": "" + "compiler_version": "", + "image_sha": "" } ], "build_type": ["Debug", "Release"], diff --git a/.github/scripts/strategy-matrix/windows.json b/.github/scripts/strategy-matrix/windows.json index 08b41e3f89..8637b31012 100644 --- a/.github/scripts/strategy-matrix/windows.json +++ b/.github/scripts/strategy-matrix/windows.json @@ -10,7 +10,8 @@ "distro_name": "windows", "distro_version": "", "compiler_name": "", - "compiler_version": "" + "compiler_version": "", + "image_sha": "" } ], "build_type": ["Debug", "Release"], diff --git a/.github/workflows/reusable-build-test.yml b/.github/workflows/reusable-build-test.yml index c274cf2b21..5bc9cf2557 100644 --- a/.github/workflows/reusable-build-test.yml +++ b/.github/workflows/reusable-build-test.yml @@ -52,7 +52,7 @@ jobs: cmake_args: ${{ matrix.cmake_args }} cmake_target: ${{ matrix.cmake_target }} runs_on: ${{ toJSON(matrix.architecture.runner) }} - image: ${{ contains(matrix.architecture.platform, 'linux') && format('ghcr.io/xrplf/ci/{0}-{1}:{2}-{3}-sha-5dd7158', matrix.os.distro_name, matrix.os.distro_version, matrix.os.compiler_name, matrix.os.compiler_version) || '' }} + image: ${{ contains(matrix.architecture.platform, 'linux') && format('ghcr.io/xrplf/ci/{0}-{1}:{2}-{3}-sha-{4}', matrix.os.distro_name, matrix.os.distro_version, matrix.os.compiler_name, matrix.os.compiler_version, matrix.os.image_sha) || '' }} config_name: ${{ matrix.config_name }} secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/upload-conan-deps.yml b/.github/workflows/upload-conan-deps.yml index cbae8a4c86..680602d978 100644 --- a/.github/workflows/upload-conan-deps.yml +++ b/.github/workflows/upload-conan-deps.yml @@ -40,11 +40,13 @@ concurrency: cancel-in-progress: true jobs: + # Generate the strategy matrix to be used by the following job. generate-matrix: uses: ./.github/workflows/reusable-strategy-matrix.yml with: strategy_matrix: ${{ github.event_name == 'pull_request' && 'minimal' || 'all' }} + # Build and upload the dependencies for each configuration. run-upload-conan-deps: needs: - generate-matrix @@ -53,8 +55,7 @@ jobs: matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }} max-parallel: 10 runs-on: ${{ matrix.architecture.runner }} - container: ${{ contains(matrix.architecture.platform, 'linux') && format('ghcr.io/xrplf/ci/{0}-{1}:{2}-{3}-sha-5dd7158', matrix.os.distro_name, matrix.os.distro_version, matrix.os.compiler_name, matrix.os.compiler_version) || null }} - + container: ${{ contains(matrix.architecture.platform, 'linux') && format('ghcr.io/xrplf/ci/{0}-{1}:{2}-{3}-sha-{4}', matrix.os.distro_name, matrix.os.distro_version, matrix.os.compiler_name, matrix.os.compiler_version, matrix.os.image_sha) || null }} steps: - name: Cleanup workspace if: ${{ runner.os == 'macOS' }} diff --git a/include/xrpl/basics/IntrusivePointer.ipp b/include/xrpl/basics/IntrusivePointer.ipp index 1ac3f2bab4..4d037bc329 100644 --- a/include/xrpl/basics/IntrusivePointer.ipp +++ b/include/xrpl/basics/IntrusivePointer.ipp @@ -654,12 +654,14 @@ SharedWeakUnion::convertToWeak() break; case destroy: // We just added a weak ref. How could we destroy? + // LCOV_EXCL_START UNREACHABLE( "ripple::SharedWeakUnion::convertToWeak : destroying freshly " "added ref"); delete p; unsafeSetRawPtr(nullptr); return true; // Should never happen + // LCOV_EXCL_STOP case partialDestroy: // This is a weird case. We just converted the last strong // pointer to a weak pointer. diff --git a/include/xrpl/beast/net/IPAddress.h b/include/xrpl/beast/net/IPAddress.h index fb5dac90ec..f3c7387bb8 100644 --- a/include/xrpl/beast/net/IPAddress.h +++ b/include/xrpl/beast/net/IPAddress.h @@ -94,7 +94,11 @@ hash_append(Hasher& h, beast::IP::Address const& addr) noexcept else if (addr.is_v6()) hash_append(h, addr.to_v6().to_bytes()); else + { + // LCOV_EXCL_START UNREACHABLE("beast::hash_append : invalid address type"); + // LCOV_EXCL_STOP + } } } // namespace beast diff --git a/include/xrpl/ledger/ApplyView.h b/include/xrpl/ledger/ApplyView.h index a1b2a2348a..c00d040e09 100644 --- a/include/xrpl/ledger/ApplyView.h +++ b/include/xrpl/ledger/ApplyView.h @@ -284,12 +284,14 @@ public: { if (key.type != ltOFFER) { + // LCOV_EXCL_START UNREACHABLE( "ripple::ApplyView::dirAppend : only Offers are appended to " "book directories"); // Only Offers are appended to book directories. Call dirInsert() // instead return std::nullopt; + // LCOV_EXCL_STOP } return dirAdd(true, directory, key.key, describe); } diff --git a/include/xrpl/protocol/detail/b58_utils.h b/include/xrpl/protocol/detail/b58_utils.h index ecd301524f..3908822661 100644 --- a/include/xrpl/protocol/detail/b58_utils.h +++ b/include/xrpl/protocol/detail/b58_utils.h @@ -129,10 +129,12 @@ inplace_bigint_div_rem(std::span numerator, std::uint64_t divisor) { // should never happen, but if it does then it seems natural to define // the a null set of numbers to be zero, so the remainder is also zero. + // LCOV_EXCL_START UNREACHABLE( "ripple::b58_fast::detail::inplace_bigint_div_rem : empty " "numerator"); return 0; + // LCOV_EXCL_STOP } auto to_u128 = [](std::uint64_t high, diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index ce194d0e2c..1d14ae0379 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -851,7 +851,7 @@ TRANSACTION(ttDELEGATE_SET, 64, DelegateSet, TRANSACTION(ttVAULT_CREATE, 65, VaultCreate, Delegation::delegatable, featureSingleAssetVault, - createPseudoAcct | createMPTIssuance, + createPseudoAcct | createMPTIssuance | mustModifyVault, ({ {sfAsset, soeREQUIRED, soeMPTSupported}, {sfAssetsMaximum, soeOPTIONAL}, @@ -869,7 +869,7 @@ TRANSACTION(ttVAULT_CREATE, 65, VaultCreate, TRANSACTION(ttVAULT_SET, 66, VaultSet, Delegation::delegatable, featureSingleAssetVault, - noPriv, + mustModifyVault, ({ {sfVaultID, soeREQUIRED}, {sfAssetsMaximum, soeOPTIONAL}, @@ -884,7 +884,7 @@ TRANSACTION(ttVAULT_SET, 66, VaultSet, TRANSACTION(ttVAULT_DELETE, 67, VaultDelete, Delegation::delegatable, featureSingleAssetVault, - mustDeleteAcct | destroyMPTIssuance, + mustDeleteAcct | destroyMPTIssuance | mustModifyVault, ({ {sfVaultID, soeREQUIRED}, })) @@ -896,7 +896,7 @@ TRANSACTION(ttVAULT_DELETE, 67, VaultDelete, TRANSACTION(ttVAULT_DEPOSIT, 68, VaultDeposit, Delegation::delegatable, featureSingleAssetVault, - mayAuthorizeMPT, + mayAuthorizeMPT | mustModifyVault, ({ {sfVaultID, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported}, @@ -909,7 +909,7 @@ TRANSACTION(ttVAULT_DEPOSIT, 68, VaultDeposit, TRANSACTION(ttVAULT_WITHDRAW, 69, VaultWithdraw, Delegation::delegatable, featureSingleAssetVault, - mayDeleteMPT, + mayDeleteMPT | mustModifyVault, ({ {sfVaultID, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported}, @@ -924,7 +924,7 @@ TRANSACTION(ttVAULT_WITHDRAW, 69, VaultWithdraw, TRANSACTION(ttVAULT_CLAWBACK, 70, VaultClawback, Delegation::delegatable, featureSingleAssetVault, - mayDeleteMPT, + mayDeleteMPT | mustModifyVault, ({ {sfVaultID, soeREQUIRED}, {sfHolder, soeREQUIRED}, diff --git a/include/xrpl/resource/detail/Logic.h b/include/xrpl/resource/detail/Logic.h index b07ee00e73..0fc5a9035a 100644 --- a/include/xrpl/resource/detail/Logic.h +++ b/include/xrpl/resource/detail/Logic.h @@ -436,10 +436,12 @@ public: admin_.erase(admin_.iterator_to(entry)); break; default: + // LCOV_EXCL_START UNREACHABLE( "ripple::Resource::Logic::release : invalid entry " "kind"); break; + // LCOV_EXCL_STOP } inactive_.push_back(entry); entry.whenExpires = m_clock.now() + secondsUntilExpiration; diff --git a/src/libxrpl/basics/Log.cpp b/src/libxrpl/basics/Log.cpp index 14873a3fd7..95419dda20 100644 --- a/src/libxrpl/basics/Log.cpp +++ b/src/libxrpl/basics/Log.cpp @@ -239,9 +239,11 @@ Logs::fromSeverity(beast::severities::Severity level) case kError: return lsERROR; + // LCOV_EXCL_START default: UNREACHABLE("ripple::Logs::fromSeverity : invalid severity"); [[fallthrough]]; + // LCOV_EXCL_STOP case kFatal: break; } @@ -265,9 +267,11 @@ Logs::toSeverity(LogSeverity level) return kWarning; case lsERROR: return kError; + // LCOV_EXCL_START default: UNREACHABLE("ripple::Logs::toSeverity : invalid severity"); [[fallthrough]]; + // LCOV_EXCL_STOP case lsFATAL: break; } @@ -292,9 +296,11 @@ Logs::toString(LogSeverity s) return "Error"; case lsFATAL: return "Fatal"; + // LCOV_EXCL_START default: UNREACHABLE("ripple::Logs::toString : invalid severity"); return "Unknown"; + // LCOV_EXCL_STOP } } @@ -356,9 +362,11 @@ Logs::format( case kError: output += "ERR "; break; + // LCOV_EXCL_START default: UNREACHABLE("ripple::Logs::format : invalid severity"); [[fallthrough]]; + // LCOV_EXCL_STOP case kFatal: output += "FTL "; break; diff --git a/src/libxrpl/basics/contract.cpp b/src/libxrpl/basics/contract.cpp index b5a7b3f368..ea75929be0 100644 --- a/src/libxrpl/basics/contract.cpp +++ b/src/libxrpl/basics/contract.cpp @@ -36,6 +36,7 @@ LogThrow(std::string const& title) [[noreturn]] void LogicError(std::string const& s) noexcept { + // LCOV_EXCL_START JLOG(debugLog().fatal()) << s; std::cerr << "Logic error: " << s << std::endl; // Use a non-standard contract naming here (without namespace) because @@ -45,6 +46,7 @@ LogicError(std::string const& s) noexcept // For the above reasons, we want this contract to stand out. UNREACHABLE("LogicError", {{"message", s}}); std::abort(); + // LCOV_EXCL_STOP } } // namespace ripple diff --git a/src/libxrpl/json/Object.cpp b/src/libxrpl/json/Object.cpp index 62f686e228..55e573c0db 100644 --- a/src/libxrpl/json/Object.cpp +++ b/src/libxrpl/json/Object.cpp @@ -174,7 +174,7 @@ Array::append(Json::Value const& v) return; } } - UNREACHABLE("Json::Array::append : invalid type"); + UNREACHABLE("Json::Array::append : invalid type"); // LCOV_EXCL_LINE } void @@ -209,7 +209,7 @@ Object::set(std::string const& k, Json::Value const& v) return; } } - UNREACHABLE("Json::Object::set : invalid type"); + UNREACHABLE("Json::Object::set : invalid type"); // LCOV_EXCL_LINE } //------------------------------------------------------------------------------ diff --git a/src/libxrpl/json/json_value.cpp b/src/libxrpl/json/json_value.cpp index 1df8f6cf31..7e4d8b6d81 100644 --- a/src/libxrpl/json/json_value.cpp +++ b/src/libxrpl/json/json_value.cpp @@ -213,8 +213,10 @@ Value::Value(ValueType type) : type_(type), allocated_(0) value_.bool_ = false; break; + // LCOV_EXCL_START default: UNREACHABLE("Json::Value::Value(ValueType) : invalid type"); + // LCOV_EXCL_STOP } } @@ -290,8 +292,10 @@ Value::Value(Value const& other) : type_(other.type_) value_.map_ = new ObjectValues(*other.value_.map_); break; + // LCOV_EXCL_START default: UNREACHABLE("Json::Value::Value(Value const&) : invalid type"); + // LCOV_EXCL_STOP } } @@ -318,8 +322,10 @@ Value::~Value() delete value_.map_; break; + // LCOV_EXCL_START default: UNREACHABLE("Json::Value::~Value : invalid type"); + // LCOV_EXCL_STOP } } @@ -419,8 +425,10 @@ operator<(Value const& x, Value const& y) return *x.value_.map_ < *y.value_.map_; } + // LCOV_EXCL_START default: UNREACHABLE("Json::operator<(Value, Value) : invalid type"); + // LCOV_EXCL_STOP } return 0; // unreachable @@ -465,8 +473,10 @@ operator==(Value const& x, Value const& y) return x.value_.map_->size() == y.value_.map_->size() && *x.value_.map_ == *y.value_.map_; + // LCOV_EXCL_START default: UNREACHABLE("Json::operator==(Value, Value) : invalid type"); + // LCOV_EXCL_STOP } return 0; // unreachable @@ -506,8 +516,10 @@ Value::asString() const case objectValue: JSON_ASSERT_MESSAGE(false, "Type is not convertible to string"); + // LCOV_EXCL_START default: UNREACHABLE("Json::Value::asString : invalid type"); + // LCOV_EXCL_STOP } return ""; // unreachable @@ -548,8 +560,10 @@ Value::asInt() const case objectValue: JSON_ASSERT_MESSAGE(false, "Type is not convertible to int"); + // LCOV_EXCL_START default: UNREACHABLE("Json::Value::asInt : invalid type"); + // LCOV_EXCL_STOP } return 0; // unreachable; @@ -590,8 +604,10 @@ Value::asUInt() const case objectValue: JSON_ASSERT_MESSAGE(false, "Type is not convertible to uint"); + // LCOV_EXCL_START default: UNREACHABLE("Json::Value::asUInt : invalid type"); + // LCOV_EXCL_STOP } return 0; // unreachable; @@ -622,8 +638,10 @@ Value::asDouble() const case objectValue: JSON_ASSERT_MESSAGE(false, "Type is not convertible to double"); + // LCOV_EXCL_START default: UNREACHABLE("Json::Value::asDouble : invalid type"); + // LCOV_EXCL_STOP } return 0; // unreachable; @@ -654,8 +672,10 @@ Value::asBool() const case objectValue: return value_.map_->size() != 0; + // LCOV_EXCL_START default: UNREACHABLE("Json::Value::asBool : invalid type"); + // LCOV_EXCL_STOP } return false; // unreachable; @@ -710,8 +730,10 @@ Value::isConvertibleTo(ValueType other) const return other == objectValue || (other == nullValue && value_.map_->size() == 0); + // LCOV_EXCL_START default: UNREACHABLE("Json::Value::isConvertible : invalid type"); + // LCOV_EXCL_STOP } return false; // unreachable; @@ -744,8 +766,10 @@ Value::size() const case objectValue: return Int(value_.map_->size()); + // LCOV_EXCL_START default: UNREACHABLE("Json::Value::size : invalid type"); + // LCOV_EXCL_STOP } return 0; // unreachable; diff --git a/src/libxrpl/ledger/ApplyStateTable.cpp b/src/libxrpl/ledger/ApplyStateTable.cpp index 7b041939d4..aaad056c58 100644 --- a/src/libxrpl/ledger/ApplyStateTable.cpp +++ b/src/libxrpl/ledger/ApplyStateTable.cpp @@ -259,9 +259,11 @@ ApplyStateTable::apply( } else { + // LCOV_EXCL_START UNREACHABLE( "ripple::detail::ApplyStateTable::apply : unsupported " "operation type"); + // LCOV_EXCL_STOP } } diff --git a/src/libxrpl/ledger/ApplyView.cpp b/src/libxrpl/ledger/ApplyView.cpp index 3c15156cf7..f02583d0d1 100644 --- a/src/libxrpl/ledger/ApplyView.cpp +++ b/src/libxrpl/ledger/ApplyView.cpp @@ -187,8 +187,10 @@ ApplyView::emptyDirDelete(Keylet const& directory) if (directory.type != ltDIR_NODE || node->getFieldH256(sfRootIndex) != directory.key) { + // LCOV_EXCL_START UNREACHABLE("ripple::ApplyView::emptyDirDelete : invalid node type"); return false; + // LCOV_EXCL_STOP } // The directory still contains entries and so it cannot be removed diff --git a/src/libxrpl/ledger/BookDirs.cpp b/src/libxrpl/ledger/BookDirs.cpp index f777d23aca..61ec160b1b 100644 --- a/src/libxrpl/ledger/BookDirs.cpp +++ b/src/libxrpl/ledger/BookDirs.cpp @@ -36,7 +36,9 @@ BookDirs::BookDirs(ReadView const& view, Book const& book) { if (!cdirFirst(*view_, key_, sle_, entry_, index_)) { + // LCOV_EXCL_START UNREACHABLE("ripple::BookDirs::BookDirs : directory is empty"); + // LCOV_EXCL_STOP } } } @@ -110,9 +112,11 @@ BookDirs::const_iterator::operator++() } else if (!cdirFirst(*view_, cur_key_, sle_, entry_, index_)) { + // LCOV_EXCL_START UNREACHABLE( "ripple::BookDirs::const_iterator::operator++ : directory is " "empty"); + // LCOV_EXCL_STOP } } diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index f9a4aeb1c4..25da88ecee 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -324,10 +324,12 @@ isVaultPseudoAccountFrozen( auto const issuer = mptIssuance->getAccountID(sfIssuer); auto const mptIssuer = view.read(keylet::account(issuer)); if (mptIssuer == nullptr) - { // LCOV_EXCL_START + { + // LCOV_EXCL_START UNREACHABLE("ripple::isVaultPseudoAccountFrozen : null MPToken issuer"); return false; - } // LCOV_EXCL_STOP + // LCOV_EXCL_STOP + } if (!mptIssuer->isFieldPresent(sfVaultID)) return false; // not a Vault pseudo-account, common case @@ -338,7 +340,8 @@ isVaultPseudoAccountFrozen( { // LCOV_EXCL_START UNREACHABLE("ripple::isVaultPseudoAccountFrozen : null vault"); return false; - } // LCOV_EXCL_STOP + // LCOV_EXCL_STOP + } return isAnyFrozen(view, {issuer, account}, vault->at(sfAsset), depth + 1); } @@ -2682,7 +2685,8 @@ enforceMPTokenAuthorization( UNREACHABLE( "ripple::enforceMPTokenAuthorization : condition list is incomplete"); return tefINTERNAL; -} // LCOV_EXCL_STOP + // LCOV_EXCL_STOP +} TER canTransfer( diff --git a/src/libxrpl/protocol/BuildInfo.cpp b/src/libxrpl/protocol/BuildInfo.cpp index b54c74c80d..0cee2995db 100644 --- a/src/libxrpl/protocol/BuildInfo.cpp +++ b/src/libxrpl/protocol/BuildInfo.cpp @@ -36,7 +36,7 @@ namespace BuildInfo { // and follow the format described at http://semver.org/ //------------------------------------------------------------------------------ // clang-format off -char const* const versionString = "2.6.1" +char const* const versionString = "3.0.0-b1" // clang-format on #if defined(DEBUG) || defined(SANITIZER) diff --git a/src/libxrpl/protocol/STBase.cpp b/src/libxrpl/protocol/STBase.cpp index 417b7e2302..ecbe6833d0 100644 --- a/src/libxrpl/protocol/STBase.cpp +++ b/src/libxrpl/protocol/STBase.cpp @@ -112,7 +112,9 @@ void STBase::add(Serializer& s) const { // Should never be called + // LCOV_EXCL_START UNREACHABLE("ripple::STBase::add : not implemented"); + // LCOV_EXCL_STOP } bool diff --git a/src/libxrpl/protocol/TxMeta.cpp b/src/libxrpl/protocol/TxMeta.cpp index 2343a6a794..833a0677b9 100644 --- a/src/libxrpl/protocol/TxMeta.cpp +++ b/src/libxrpl/protocol/TxMeta.cpp @@ -238,9 +238,11 @@ TxMeta::getAffectedNode(uint256 const& node) if (n.getFieldH256(sfLedgerIndex) == node) return n; } + // LCOV_EXCL_START UNREACHABLE("ripple::TxMeta::getAffectedNode(uint256) : node not found"); Throw("Affected node not found"); return *(mNodes.begin()); // Silence compiler warning. + // LCOV_EXCL_STOP } STObject diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index f279c8dec2..d2b716ef04 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -24,9 +24,18 @@ #include #include +#include #include +#include +#include #include +#include +#include #include +#include +#include +#include +#include #include @@ -66,8 +75,9 @@ class Invariants_test : public beast::unit_test::suite * checker. * @preclose See "Preclose" above. Note that @preclose runs *before* * @precheck, but is the last parameter for historical reasons - * + * @setTxAccount optionally set to add sfAccount to tx (either A1 or A2) */ + enum class TxAccount : int { None = 0, A1, A2 }; void doInvariantCheck( std::vector const& expect_logs, @@ -76,7 +86,8 @@ class Invariants_test : public beast::unit_test::suite STTx tx = STTx{ttACCOUNT_SET, [](STObject&) {}}, std::initializer_list ters = {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, - Preclose const& preclose = {}) + Preclose const& preclose = {}, + TxAccount setTxAccount = TxAccount::None) { using namespace test::jtx; FeatureBitset amendments = testable_amendments() | @@ -93,6 +104,9 @@ class Invariants_test : public beast::unit_test::suite OpenView ov{*env.current()}; test::StreamSink sink{beast::severities::kWarning}; beast::Journal jlog{sink}; + if (setTxAccount != TxAccount::None) + tx.setAccountID( + sfAccount, setTxAccount == TxAccount::A1 ? A1.id() : A2.id()); ApplyContext ac{ env.app(), ov, @@ -117,12 +131,13 @@ class Invariants_test : public beast::unit_test::suite BEAST_EXPECT( messages.starts_with("Invariant failed:") || messages.starts_with("Transaction caused an exception")); + // std::cerr << messages << '\n'; for (auto const& m : expect_logs) { if (messages.find(m) == std::string::npos) { // uncomment if you want to log the invariant failure - // message log << " --> " << m << std::endl; + // std::cerr << " --> " << m << std::endl; fail(); } } @@ -2147,6 +2162,1690 @@ class Invariants_test : public beast::unit_test::suite } } + void + testVault() + { + using namespace test::jtx; + + struct AccountAmount + { + AccountID account; + int amount; + }; + struct Adjustements + { + std::optional assetsTotal = {}; + std::optional assetsAvailable = {}; + std::optional lossUnrealized = {}; + std::optional assetsMaximum = {}; + std::optional sharesTotal = {}; + std::optional vaultAssets = {}; + std::optional accountAssets = {}; + std::optional accountShares = {}; + }; + auto constexpr adjust = [&](ApplyView& ac, + ripple::Keylet keylet, + Adjustements args) { + auto sleVault = ac.peek(keylet); + if (!sleVault) + return false; + + auto const mptIssuanceID = (*sleVault)[sfShareMPTID]; + auto sleShares = ac.peek(keylet::mptIssuance(mptIssuanceID)); + if (!sleShares) + return false; + + // These two fields are adjusted in absolute terms + if (args.lossUnrealized) + (*sleVault)[sfLossUnrealized] = *args.lossUnrealized; + if (args.assetsMaximum) + (*sleVault)[sfAssetsMaximum] = *args.assetsMaximum; + + // Remaining fields are adjusted in terms of difference + if (args.assetsTotal) + (*sleVault)[sfAssetsTotal] = + *(*sleVault)[sfAssetsTotal] + *args.assetsTotal; + if (args.assetsAvailable) + (*sleVault)[sfAssetsAvailable] = + *(*sleVault)[sfAssetsAvailable] + *args.assetsAvailable; + ac.update(sleVault); + + if (args.sharesTotal) + (*sleShares)[sfOutstandingAmount] = + *(*sleShares)[sfOutstandingAmount] + *args.sharesTotal; + ac.update(sleShares); + + auto const assets = *(*sleVault)[sfAsset]; + auto const pseudoId = *(*sleVault)[sfAccount]; + if (args.vaultAssets) + { + if (assets.native()) + { + auto slePseudoAccount = ac.peek(keylet::account(pseudoId)); + if (!slePseudoAccount) + return false; + (*slePseudoAccount)[sfBalance] = + *(*slePseudoAccount)[sfBalance] + *args.vaultAssets; + ac.update(slePseudoAccount); + } + else if (assets.holds()) + { + auto const mptId = assets.get().getMptID(); + auto sleMPToken = ac.peek(keylet::mptoken(mptId, pseudoId)); + if (!sleMPToken) + return false; + (*sleMPToken)[sfMPTAmount] = + *(*sleMPToken)[sfMPTAmount] + *args.vaultAssets; + ac.update(sleMPToken); + } + else + return false; // Not supporting testing with IOU + } + + if (args.accountAssets) + { + auto const& pair = *args.accountAssets; + if (assets.native()) + { + auto sleAccount = ac.peek(keylet::account(pair.account)); + if (!sleAccount) + return false; + (*sleAccount)[sfBalance] = + *(*sleAccount)[sfBalance] + pair.amount; + ac.update(sleAccount); + } + else if (assets.holds()) + { + auto const mptID = assets.get().getMptID(); + auto sleMPToken = + ac.peek(keylet::mptoken(mptID, pair.account)); + if (!sleMPToken) + return false; + (*sleMPToken)[sfMPTAmount] = + *(*sleMPToken)[sfMPTAmount] + pair.amount; + ac.update(sleMPToken); + } + else + return false; // Not supporting testing with IOU + } + + if (args.accountShares) + { + auto const& pair = *args.accountShares; + auto sleMPToken = + ac.peek(keylet::mptoken(mptIssuanceID, pair.account)); + if (!sleMPToken) + return false; + (*sleMPToken)[sfMPTAmount] = + *(*sleMPToken)[sfMPTAmount] + pair.amount; + ac.update(sleMPToken); + } + return true; + }; + + constexpr auto args = + [](AccountID id, int adjustement, auto fn) -> Adjustements { + Adjustements sample = { + .assetsTotal = adjustement, + .assetsAvailable = adjustement, + .lossUnrealized = 0, + .sharesTotal = adjustement, + .vaultAssets = adjustement, + .accountAssets = // + AccountAmount{id, -adjustement}, + .accountShares = // + AccountAmount{id, adjustement}}; + fn(sample); + return sample; + }; + + Account A3{"A3"}; + Account A4{"A4"}; + auto const precloseXrp = + [&](Account const& A1, Account const& A2, Env& env) -> bool { + env.fund(XRP(1000), A3, A4); + Vault vault{env}; + auto [tx, keylet] = + vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + env(vault.deposit( + {.depositor = A1, .id = keylet.key, .amount = XRP(10)})); + env(vault.deposit( + {.depositor = A2, .id = keylet.key, .amount = XRP(10)})); + env(vault.deposit( + {.depositor = A3, .id = keylet.key, .amount = XRP(10)})); + return true; + }; + + testcase << "Vault general checks"; + doInvariantCheck( + {"vault deletion succeeded without deleting a vault"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + ac.view().update(sleVault); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_DELETE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + auto [tx, _] = vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + return true; + }); + + doInvariantCheck( + {"vault updated by a wrong transaction type"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + ac.view().erase(sleVault); + return true; + }, + XRPAmount{}, + STTx{ttPAYMENT, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + auto [tx, _] = vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + return true; + }); + + doInvariantCheck( + {"vault updated by a wrong transaction type"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + ac.view().update(sleVault); + return true; + }, + XRPAmount{}, + STTx{ttPAYMENT, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + auto [tx, _] = vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + return true; + }); + + doInvariantCheck( + {"vault updated by a wrong transaction type"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const sequence = ac.view().seq(); + auto const vaultKeylet = keylet::vault(A1.id(), sequence); + auto sleVault = std::make_shared(vaultKeylet); + auto const vaultPage = ac.view().dirInsert( + keylet::ownerDir(A1.id()), + sleVault->key(), + describeOwnerDir(A1.id())); + sleVault->setFieldU64(sfOwnerNode, *vaultPage); + ac.view().insert(sleVault); + return true; + }, + XRPAmount{}, + STTx{ttPAYMENT, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}); + + doInvariantCheck( + {"vault deleted by a wrong transaction type"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + ac.view().erase(sleVault); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_SET, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + auto [tx, _] = vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + return true; + }); + + doInvariantCheck( + {"vault operation updated more than single vault"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + ac.view().erase(sleVault); + } + { + auto const keylet = keylet::vault(A2.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + ac.view().erase(sleVault); + } + return true; + }, + XRPAmount{}, + STTx{ttVAULT_DELETE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + { + auto [tx, _] = + vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + } + { + auto [tx, _] = + vault.create({.owner = A2, .asset = xrpIssue()}); + env(tx); + } + return true; + }); + + doInvariantCheck( + {"vault operation updated more than single vault"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const sequence = ac.view().seq(); + auto const insertVault = [&](Account const A) { + auto const vaultKeylet = keylet::vault(A.id(), sequence); + auto sleVault = std::make_shared(vaultKeylet); + auto const vaultPage = ac.view().dirInsert( + keylet::ownerDir(A.id()), + sleVault->key(), + describeOwnerDir(A.id())); + sleVault->setFieldU64(sfOwnerNode, *vaultPage); + ac.view().insert(sleVault); + }; + insertVault(A1); + insertVault(A2); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_CREATE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}); + + doInvariantCheck( + {"deleted vault must also delete shares"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + ac.view().erase(sleVault); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_DELETE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + auto [tx, _] = vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + return true; + }); + + doInvariantCheck( + {"deleted vault must have no shares outstanding", + "deleted vault must have no assets outstanding", + "deleted vault must have no assets available"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + auto sleShares = ac.view().peek( + keylet::mptIssuance((*sleVault)[sfShareMPTID])); + if (!sleShares) + return false; + ac.view().erase(sleVault); + ac.view().erase(sleShares); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_DELETE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + auto [tx, keylet] = + vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + env(vault.deposit( + {.depositor = A1, .id = keylet.key, .amount = XRP(10)})); + return true; + }); + + doInvariantCheck( + {"vault operation succeeded without modifying a vault"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + auto sleShares = ac.view().peek( + keylet::mptIssuance((*sleVault)[sfShareMPTID])); + if (!sleShares) + return false; + // Note, such an "orphaned" update of MPT issuance attached to a + // vault is invalid; ttVAULT_SET must also update Vault object. + sleShares->setFieldH256(sfDomainID, uint256(13)); + ac.view().update(sleShares); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_SET, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + doInvariantCheck( + {"vault operation succeeded without modifying a vault"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + return true; + }, + XRPAmount{}, + STTx{ttVAULT_CREATE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + auto [tx, _] = vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + return true; + }); + + doInvariantCheck( + {"vault operation succeeded without modifying a vault"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + return true; + }, + XRPAmount{}, + STTx{ttVAULT_DEPOSIT, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + auto [tx, _] = vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + return true; + }); + + doInvariantCheck( + {"vault operation succeeded without modifying a vault"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + return true; + }, + XRPAmount{}, + STTx{ttVAULT_WITHDRAW, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + auto [tx, _] = vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + return true; + }); + + doInvariantCheck( + {"vault operation succeeded without modifying a vault"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + return true; + }, + XRPAmount{}, + STTx{ttVAULT_CLAWBACK, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + auto [tx, _] = vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + return true; + }); + + doInvariantCheck( + {"vault operation succeeded without modifying a vault"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + return true; + }, + XRPAmount{}, + STTx{ttVAULT_DELETE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + auto [tx, _] = vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + return true; + }); + + doInvariantCheck( + {"updated vault must have shares"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + (*sleVault)[sfAssetsMaximum] = 200; + ac.view().update(sleVault); + + auto sleShares = ac.view().peek( + keylet::mptIssuance((*sleVault)[sfShareMPTID])); + if (!sleShares) + return false; + ac.view().erase(sleShares); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_SET, [](STObject&) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + auto [tx, _] = vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + return true; + }); + + doInvariantCheck( + {"vault operation succeeded without updating shares", + "assets available must not be greater than assets outstanding"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + (*sleVault)[sfAssetsTotal] = 9; + ac.view().update(sleVault); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_WITHDRAW, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + auto [tx, keylet] = + vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + env(vault.deposit( + {.depositor = A1, .id = keylet.key, .amount = XRP(10)})); + return true; + }); + + doInvariantCheck( + {"set must not change assets outstanding", + "set must not change assets available", + "set must not change shares outstanding", + "set must not change vault balance", + "assets available must be positive", + "assets available must not be greater than assets outstanding", + "assets outstanding must be positive"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + auto slePseudoAccount = + ac.view().peek(keylet::account(*(*sleVault)[sfAccount])); + if (!slePseudoAccount) + return false; + (*slePseudoAccount)[sfBalance] = + *(*slePseudoAccount)[sfBalance] - 10; + ac.view().update(slePseudoAccount); + + // Move 10 drops to A4 to enforce total XRP balance + auto sleA4 = ac.view().peek(keylet::account(A4.id())); + if (!sleA4) + return false; + (*sleA4)[sfBalance] = *(*sleA4)[sfBalance] + 10; + ac.view().update(sleA4); + + return adjust( + ac.view(), + keylet, + args(A2.id(), 0, [&](Adjustements& sample) { + sample.assetsAvailable = (DROPS_PER_XRP * -100).value(); + sample.assetsTotal = (DROPS_PER_XRP * -200).value(); + sample.sharesTotal = -1; + })); + }, + XRPAmount{}, + STTx{ttVAULT_SET, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + doInvariantCheck( + {"violation of vault immutable data"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + sleVault->setFieldIssue( + sfAsset, STIssue{sfAsset, MPTIssue(MPTID(42))}); + ac.view().update(sleVault); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_SET, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp); + + doInvariantCheck( + {"violation of vault immutable data"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + sleVault->setAccountID(sfAccount, A2.id()); + ac.view().update(sleVault); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_SET, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp); + + doInvariantCheck( + {"violation of vault immutable data"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + (*sleVault)[sfShareMPTID] = MPTID(42); + ac.view().update(sleVault); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_SET, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp); + + doInvariantCheck( + {"vault transaction must not change loss unrealized", + "set must not change assets outstanding"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + return adjust( + ac.view(), + keylet, + args(A2.id(), 0, [&](Adjustements& sample) { + sample.lossUnrealized = 13; + sample.assetsTotal = 20; + })); + }, + XRPAmount{}, + STTx{ttVAULT_SET, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + doInvariantCheck( + {"loss unrealized must not exceed the difference " + "between assets outstanding and available", + "vault transaction must not change loss unrealized"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + return adjust( + ac.view(), + keylet, + args(A2.id(), 100, [&](Adjustements& sample) { + sample.lossUnrealized = 13; + })); + }, + XRPAmount{}, + STTx{ + ttVAULT_DEPOSIT, + [](STObject& tx) { + tx.setFieldAmount(sfAmount, XRPAmount(200)); + }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + doInvariantCheck( + {"set assets outstanding must not exceed assets maximum"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + return adjust( + ac.view(), + keylet, + args(A2.id(), 0, [&](Adjustements& sample) { + sample.assetsMaximum = 1; + })); + }, + XRPAmount{}, + STTx{ttVAULT_SET, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + doInvariantCheck( + {"assets maximum must be positive"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + return adjust( + ac.view(), + keylet, + args(A2.id(), 0, [&](Adjustements& sample) { + sample.assetsMaximum = -1; + })); + }, + XRPAmount{}, + STTx{ttVAULT_SET, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + doInvariantCheck( + {"set must not change shares outstanding", + "updated zero sized vault must have no assets outstanding", + "updated zero sized vault must have no assets available"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + ac.view().update(sleVault); + auto sleShares = ac.view().peek( + keylet::mptIssuance((*sleVault)[sfShareMPTID])); + if (!sleShares) + return false; + (*sleShares)[sfOutstandingAmount] = 0; + ac.view().update(sleShares); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_SET, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + doInvariantCheck( + {"updated shares must not exceed maximum"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + auto sleShares = ac.view().peek( + keylet::mptIssuance((*sleVault)[sfShareMPTID])); + if (!sleShares) + return false; + (*sleShares)[sfMaximumAmount] = 10; + ac.view().update(sleShares); + + return adjust( + ac.view(), keylet, args(A2.id(), 10, [](Adjustements&) {})); + }, + XRPAmount{}, + STTx{ttVAULT_DEPOSIT, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + doInvariantCheck( + {"updated shares must not exceed maximum"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + adjust( + ac.view(), keylet, args(A2.id(), 10, [](Adjustements&) {})); + + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + auto sleShares = ac.view().peek( + keylet::mptIssuance((*sleVault)[sfShareMPTID])); + if (!sleShares) + return false; + (*sleShares)[sfOutstandingAmount] = maxMPTokenAmount + 1; + ac.view().update(sleShares); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_DEPOSIT, [](STObject&) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + testcase << "Vault create"; + doInvariantCheck( + { + "created vault must be empty", + "updated zero sized vault must have no assets outstanding", + "create operation must not have updated a vault", + }, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + (*sleVault)[sfAssetsTotal] = 9; + ac.view().update(sleVault); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_CREATE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + auto [tx, keylet] = + vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + return true; + }); + + doInvariantCheck( + { + "created vault must be empty", + "updated zero sized vault must have no assets available", + "assets available must not be greater than assets outstanding", + "create operation must not have updated a vault", + }, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + (*sleVault)[sfAssetsAvailable] = 9; + ac.view().update(sleVault); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_CREATE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + auto [tx, keylet] = + vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + return true; + }); + + doInvariantCheck( + { + "created vault must be empty", + "loss unrealized must not exceed the difference between assets " + "outstanding and available", + "vault transaction must not change loss unrealized", + "create operation must not have updated a vault", + }, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + (*sleVault)[sfLossUnrealized] = 1; + ac.view().update(sleVault); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_CREATE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + auto [tx, keylet] = + vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + return true; + }); + + doInvariantCheck( + { + "created vault must be empty", + "create operation must not have updated a vault", + }, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + auto sleShares = ac.view().peek( + keylet::mptIssuance((*sleVault)[sfShareMPTID])); + if (!sleShares) + return false; + ac.view().update(sleVault); + (*sleShares)[sfOutstandingAmount] = 9; + ac.view().update(sleShares); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_CREATE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + auto [tx, keylet] = + vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + return true; + }); + + doInvariantCheck( + { + "assets maximum must be positive", + "create operation must not have updated a vault", + }, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + (*sleVault)[sfAssetsMaximum] = Number(-1); + ac.view().update(sleVault); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_CREATE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + auto [tx, keylet] = + vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + return true; + }); + + doInvariantCheck( + {"create operation must not have updated a vault", + "shares issuer and vault pseudo-account must be the same", + "shares issuer must be a pseudo-account", + "shares issuer pseudo-account must point back to the vault"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + auto sleShares = ac.view().peek( + keylet::mptIssuance((*sleVault)[sfShareMPTID])); + if (!sleShares) + return false; + ac.view().update(sleVault); + (*sleShares)[sfIssuer] = A1.id(); + ac.view().update(sleShares); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_CREATE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + auto [tx, keylet] = + vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + return true; + }); + + doInvariantCheck( + {"vault created by a wrong transaction type", + "account root created illegally"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + // The code below will create a valid vault with (almost) all + // the invariants holding. Except one: it is created by the + // wrong transaction type. + auto const sequence = ac.view().seq(); + auto const vaultKeylet = keylet::vault(A1.id(), sequence); + auto sleVault = std::make_shared(vaultKeylet); + auto const vaultPage = ac.view().dirInsert( + keylet::ownerDir(A1.id()), + sleVault->key(), + describeOwnerDir(A1.id())); + sleVault->setFieldU64(sfOwnerNode, *vaultPage); + + auto pseudoId = + pseudoAccountAddress(ac.view(), vaultKeylet.key); + // Create pseudo-account. + auto sleAccount = + std::make_shared(keylet::account(pseudoId)); + sleAccount->setAccountID(sfAccount, pseudoId); + sleAccount->setFieldAmount(sfBalance, STAmount{}); + std::uint32_t const seqno = // + ac.view().rules().enabled(featureSingleAssetVault) // + ? 0 // + : sequence; + sleAccount->setFieldU32(sfSequence, seqno); + sleAccount->setFieldU32( + sfFlags, + lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth); + sleAccount->setFieldH256(sfVaultID, vaultKeylet.key); + ac.view().insert(sleAccount); + + auto const sharesMptId = makeMptID(sequence, pseudoId); + auto const sharesKeylet = keylet::mptIssuance(sharesMptId); + auto sleShares = std::make_shared(sharesKeylet); + auto const sharesPage = ac.view().dirInsert( + keylet::ownerDir(pseudoId), + sharesKeylet, + describeOwnerDir(pseudoId)); + sleShares->setFieldU64(sfOwnerNode, *sharesPage); + + sleShares->at(sfFlags) = 0; + sleShares->at(sfIssuer) = pseudoId; + sleShares->at(sfOutstandingAmount) = 0; + sleShares->at(sfSequence) = sequence; + + sleVault->at(sfAccount) = pseudoId; + sleVault->at(sfFlags) = 0; + sleVault->at(sfSequence) = sequence; + sleVault->at(sfOwner) = A1.id(); + sleVault->at(sfAssetsTotal) = Number(0); + sleVault->at(sfAssetsAvailable) = Number(0); + sleVault->at(sfLossUnrealized) = Number(0); + sleVault->at(sfShareMPTID) = sharesMptId; + sleVault->at(sfWithdrawalPolicy) = + vaultStrategyFirstComeFirstServe; + + ac.view().insert(sleVault); + ac.view().insert(sleShares); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_SET, [](STObject&) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}); + + doInvariantCheck( + {"shares issuer and vault pseudo-account must be the same", + "shares issuer pseudo-account must point back to the vault"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const sequence = ac.view().seq(); + auto const vaultKeylet = keylet::vault(A1.id(), sequence); + auto sleVault = std::make_shared(vaultKeylet); + auto const vaultPage = ac.view().dirInsert( + keylet::ownerDir(A1.id()), + sleVault->key(), + describeOwnerDir(A1.id())); + sleVault->setFieldU64(sfOwnerNode, *vaultPage); + + auto pseudoId = + pseudoAccountAddress(ac.view(), vaultKeylet.key); + // Create pseudo-account. + auto sleAccount = + std::make_shared(keylet::account(pseudoId)); + sleAccount->setAccountID(sfAccount, pseudoId); + sleAccount->setFieldAmount(sfBalance, STAmount{}); + std::uint32_t const seqno = // + ac.view().rules().enabled(featureSingleAssetVault) // + ? 0 // + : sequence; + sleAccount->setFieldU32(sfSequence, seqno); + sleAccount->setFieldU32( + sfFlags, + lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth); + // sleAccount->setFieldH256(sfVaultID, vaultKeylet.key); + // Setting wrong vault key + sleAccount->setFieldH256(sfVaultID, uint256(42)); + ac.view().insert(sleAccount); + + auto const sharesMptId = makeMptID(sequence, pseudoId); + auto const sharesKeylet = keylet::mptIssuance(sharesMptId); + auto sleShares = std::make_shared(sharesKeylet); + auto const sharesPage = ac.view().dirInsert( + keylet::ownerDir(pseudoId), + sharesKeylet, + describeOwnerDir(pseudoId)); + sleShares->setFieldU64(sfOwnerNode, *sharesPage); + + sleShares->at(sfFlags) = 0; + sleShares->at(sfIssuer) = pseudoId; + sleShares->at(sfOutstandingAmount) = 0; + sleShares->at(sfSequence) = sequence; + + // sleVault->at(sfAccount) = pseudoId; + // Setting wrong pseudo acocunt ID + sleVault->at(sfAccount) = A2.id(); + sleVault->at(sfFlags) = 0; + sleVault->at(sfSequence) = sequence; + sleVault->at(sfOwner) = A1.id(); + sleVault->at(sfAssetsTotal) = Number(0); + sleVault->at(sfAssetsAvailable) = Number(0); + sleVault->at(sfLossUnrealized) = Number(0); + sleVault->at(sfShareMPTID) = sharesMptId; + sleVault->at(sfWithdrawalPolicy) = + vaultStrategyFirstComeFirstServe; + + ac.view().insert(sleVault); + ac.view().insert(sleShares); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_CREATE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}); + + doInvariantCheck( + {"shares issuer and vault pseudo-account must be the same", + "shares issuer must exist"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const sequence = ac.view().seq(); + auto const vaultKeylet = keylet::vault(A1.id(), sequence); + auto sleVault = std::make_shared(vaultKeylet); + auto const vaultPage = ac.view().dirInsert( + keylet::ownerDir(A1.id()), + sleVault->key(), + describeOwnerDir(A1.id())); + sleVault->setFieldU64(sfOwnerNode, *vaultPage); + + auto const sharesMptId = makeMptID(sequence, A2.id()); + auto const sharesKeylet = keylet::mptIssuance(sharesMptId); + auto sleShares = std::make_shared(sharesKeylet); + auto const sharesPage = ac.view().dirInsert( + keylet::ownerDir(A2.id()), + sharesKeylet, + describeOwnerDir(A2.id())); + sleShares->setFieldU64(sfOwnerNode, *sharesPage); + + sleShares->at(sfFlags) = 0; + // Setting wrong pseudo acocunt ID + sleShares->at(sfIssuer) = AccountID(uint160(42)); + sleShares->at(sfOutstandingAmount) = 0; + sleShares->at(sfSequence) = sequence; + + sleVault->at(sfAccount) = A2.id(); + sleVault->at(sfFlags) = 0; + sleVault->at(sfSequence) = sequence; + sleVault->at(sfOwner) = A1.id(); + sleVault->at(sfAssetsTotal) = Number(0); + sleVault->at(sfAssetsAvailable) = Number(0); + sleVault->at(sfLossUnrealized) = Number(0); + sleVault->at(sfShareMPTID) = sharesMptId; + sleVault->at(sfWithdrawalPolicy) = + vaultStrategyFirstComeFirstServe; + + ac.view().insert(sleVault); + ac.view().insert(sleShares); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_CREATE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}); + + testcase << "Vault deposit"; + doInvariantCheck( + {"deposit must change vault balance"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + return adjust( + ac.view(), + keylet, + args(A2.id(), 0, [&](Adjustements& sample) {})); + }, + XRPAmount{}, + STTx{ttVAULT_DEPOSIT, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp); + + doInvariantCheck( + {"deposit assets outstanding must not exceed assets maximum"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + return adjust( + ac.view(), + keylet, + args(A2.id(), 200, [&](Adjustements& sample) { + sample.assetsMaximum = 1; + })); + }, + XRPAmount{}, + STTx{ + ttVAULT_DEPOSIT, + [](STObject& tx) { + tx.setFieldAmount(sfAmount, XRPAmount(200)); + }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + // This really convoluted unit tests makes the zero balance on the + // depositor, by sending them the same amount as the transaction fee. + // The operation makes no sense, but the defensive check in + // ValidVault::finalize is otherwise impossible to trigger. + doInvariantCheck( + {"deposit must increase vault balance", + "deposit must change depositor balance"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + + // Move 10 drops to A4 to enforce total XRP balance + auto sleA4 = ac.view().peek(keylet::account(A4.id())); + if (!sleA4) + return false; + (*sleA4)[sfBalance] = *(*sleA4)[sfBalance] + 10; + ac.view().update(sleA4); + + return adjust( + ac.view(), + keylet, + args(A3.id(), -10, [&](Adjustements& sample) { + sample.accountAssets->amount = -100; + })); + }, + XRPAmount{100}, + STTx{ + ttVAULT_DEPOSIT, + [&](STObject& tx) { + tx[sfFee] = XRPAmount(100); + tx[sfAccount] = A3.id(); + }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp); + + doInvariantCheck( + {"deposit must increase vault balance", + "deposit must decrease depositor balance", + "deposit must change vault and depositor balance by equal amount", + "deposit and assets outstanding must add up", + "deposit and assets available must add up"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + + // Move 10 drops from A2 to A3 to enforce total XRP balance + auto sleA3 = ac.view().peek(keylet::account(A3.id())); + if (!sleA3) + return false; + (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] + 10; + ac.view().update(sleA3); + + return adjust( + ac.view(), + keylet, + args(A2.id(), 10, [&](Adjustements& sample) { + sample.vaultAssets = -20; + sample.accountAssets->amount = 10; + })); + }, + XRPAmount{}, + STTx{ + ttVAULT_DEPOSIT, + [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + doInvariantCheck( + {"deposit must change depositor balance"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + + // Move 10 drops from A3 to vault to enforce total XRP balance + auto sleA3 = ac.view().peek(keylet::account(A3.id())); + if (!sleA3) + return false; + (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] - 10; + ac.view().update(sleA3); + + return adjust( + ac.view(), + keylet, + args(A2.id(), 10, [&](Adjustements& sample) { + sample.accountAssets->amount = 0; + })); + }, + XRPAmount{}, + STTx{ + ttVAULT_DEPOSIT, + [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + doInvariantCheck( + {"deposit must change depositor shares"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + return adjust( + ac.view(), + keylet, + args(A2.id(), 10, [&](Adjustements& sample) { + sample.accountShares->amount = 0; + })); + }, + XRPAmount{}, + STTx{ + ttVAULT_DEPOSIT, + [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + doInvariantCheck( + {"deposit must change vault shares"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + return adjust( + ac.view(), + keylet, + args(A2.id(), 10, [&](Adjustements& sample) { + sample.sharesTotal = 0; + })); + }, + XRPAmount{}, + STTx{ + ttVAULT_DEPOSIT, + [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + doInvariantCheck( + {"deposit must increase depositor shares", + "deposit must change depositor and vault shares by equal amount", + "deposit must not change vault balance by more than deposited " + "amount"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + return adjust( + ac.view(), + keylet, + args(A2.id(), 10, [&](Adjustements& sample) { + sample.accountShares->amount = -5; + sample.sharesTotal = -10; + })); + }, + XRPAmount{}, + STTx{ + ttVAULT_DEPOSIT, + [](STObject& tx) { tx[sfAmount] = XRPAmount(5); }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + doInvariantCheck( + {"deposit and assets outstanding must add up", + "deposit and assets available must add up"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + return adjust( + ac.view(), + keylet, + args(A2.id(), 10, [&](Adjustements& sample) { + sample.assetsTotal = 7; + sample.assetsAvailable = 7; + })); + }, + XRPAmount{}, + STTx{ + ttVAULT_DEPOSIT, + [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + testcase << "Vault withdrawal"; + doInvariantCheck( + {"withdrawal must change vault balance"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + return adjust( + ac.view(), + keylet, + args(A2.id(), 0, [&](Adjustements& sample) {})); + }, + XRPAmount{}, + STTx{ttVAULT_WITHDRAW, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp); + + // Almost identical to the really convoluted test for deposit, where the + // depositor spends only the transaction fee. In case of withdrawal, + // this test is almost the same as normal withdrawal where the + // sfDestination would have been A4, but has been omitted. + doInvariantCheck( + {"withdrawal must change one destination balance"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + + // Move 10 drops to A4 to enforce total XRP balance + auto sleA4 = ac.view().peek(keylet::account(A4.id())); + if (!sleA4) + return false; + (*sleA4)[sfBalance] = *(*sleA4)[sfBalance] + 10; + ac.view().update(sleA4); + + return adjust( + ac.view(), + keylet, + args(A3.id(), -10, [&](Adjustements& sample) { + sample.accountAssets->amount = -100; + })); + }, + XRPAmount{100}, + STTx{ + ttVAULT_WITHDRAW, + [&](STObject& tx) { + tx[sfFee] = XRPAmount(100); + tx[sfAccount] = A3.id(); + // This commented out line causes the invariant violation. + // tx[sfDestination] = A4.id(); + }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp); + + doInvariantCheck( + {"withdrawal must change vault and destination balance by " + "equal amount", + "withdrawal must decrease vault balance", + "withdrawal must increase destination balance", + "withdrawal and assets outstanding must add up", + "withdrawal and assets available must add up"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + + // Move 10 drops from A2 to A3 to enforce total XRP balance + auto sleA3 = ac.view().peek(keylet::account(A3.id())); + if (!sleA3) + return false; + (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] + 10; + ac.view().update(sleA3); + + return adjust( + ac.view(), + keylet, + args(A2.id(), -10, [&](Adjustements& sample) { + sample.vaultAssets = 10; + sample.accountAssets->amount = -20; + })); + }, + XRPAmount{}, + STTx{ttVAULT_WITHDRAW, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + doInvariantCheck( + {"withdrawal must change one destination balance"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + if (!adjust( + ac.view(), + keylet, + args(A2.id(), -10, [&](Adjustements& sample) { + *sample.vaultAssets -= 5; + }))) + return false; + auto sleA3 = ac.view().peek(keylet::account(A3.id())); + if (!sleA3) + return false; + (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] + 5; + ac.view().update(sleA3); + return true; + }, + XRPAmount{}, + STTx{ + ttVAULT_WITHDRAW, + [&](STObject& tx) { tx.setAccountID(sfDestination, A3.id()); }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + doInvariantCheck( + {"withdrawal must change depositor shares"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + return adjust( + ac.view(), + keylet, + args(A2.id(), -10, [&](Adjustements& sample) { + sample.accountShares->amount = 0; + })); + }, + XRPAmount{}, + STTx{ttVAULT_WITHDRAW, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + doInvariantCheck( + {"withdrawal must change vault shares"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + return adjust( + ac.view(), + keylet, + args(A2.id(), -10, [&](Adjustements& sample) { + sample.sharesTotal = 0; + })); + }, + XRPAmount{}, + STTx{ttVAULT_WITHDRAW, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + doInvariantCheck( + {"withdrawal must decrease depositor shares", + "withdrawal must change depositor and vault shares by equal " + "amount"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + return adjust( + ac.view(), + keylet, + args(A2.id(), -10, [&](Adjustements& sample) { + sample.accountShares->amount = 5; + sample.sharesTotal = 10; + })); + }, + XRPAmount{}, + STTx{ttVAULT_WITHDRAW, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + doInvariantCheck( + {"withdrawal and assets outstanding must add up", + "withdrawal and assets available must add up"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + return adjust( + ac.view(), + keylet, + args(A2.id(), -10, [&](Adjustements& sample) { + sample.assetsTotal = -15; + sample.assetsAvailable = -15; + })); + }, + XRPAmount{}, + STTx{ttVAULT_WITHDRAW, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + auto const precloseMpt = + [&](Account const& A1, Account const& A2, Env& env) -> bool { + env.fund(XRP(1000), A3, A4); + + // Create MPT asset + { + Json::Value jv; + jv[sfAccount] = A3.human(); + jv[sfTransactionType] = jss::MPTokenIssuanceCreate; + jv[sfFlags] = tfMPTCanTransfer; + env(jv); + env.close(); + } + + auto const mptID = makeMptID(env.seq(A3) - 1, A3); + Asset asset = MPTIssue(mptID); + // Authorize A1 A2 A4 + { + Json::Value jv; + jv[sfAccount] = A1.human(); + jv[sfTransactionType] = jss::MPTokenAuthorize; + jv[sfMPTokenIssuanceID] = to_string(mptID); + env(jv); + jv[sfAccount] = A2.human(); + env(jv); + jv[sfAccount] = A4.human(); + env(jv); + + env.close(); + } + // Send tokens to A1 A2 A4 + { + env(pay(A3, A1, asset(1000))); + env(pay(A3, A2, asset(1000))); + env(pay(A3, A4, asset(1000))); + env.close(); + } + + Vault vault{env}; + auto [tx, keylet] = vault.create({.owner = A1, .asset = asset}); + env(tx); + env(vault.deposit( + {.depositor = A1, .id = keylet.key, .amount = asset(10)})); + env(vault.deposit( + {.depositor = A2, .id = keylet.key, .amount = asset(10)})); + env(vault.deposit( + {.depositor = A4, .id = keylet.key, .amount = asset(10)})); + return true; + }; + + doInvariantCheck( + {"withdrawal must decrease depositor shares", + "withdrawal must change depositor and vault shares by equal " + "amount"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq() - 2); + return adjust( + ac.view(), + keylet, + args(A2.id(), -10, [&](Adjustements& sample) { + sample.accountShares->amount = 5; + })); + }, + XRPAmount{}, + STTx{ + ttVAULT_WITHDRAW, + [&](STObject& tx) { tx[sfAccount] = A3.id(); }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseMpt, + TxAccount::A2); + + testcase << "Vault clawback"; + doInvariantCheck( + {"clawback must change vault balance"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq() - 2); + return adjust( + ac.view(), + keylet, + args(A2.id(), -1, [&](Adjustements& sample) { + sample.vaultAssets = 0; + })); + }, + XRPAmount{}, + STTx{ + ttVAULT_CLAWBACK, + [&](STObject& tx) { tx[sfAccount] = A3.id(); }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseMpt); + + // Not the same as below check: attempt to clawback XRP + doInvariantCheck( + {"clawback may only be performed by the asset issuer"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + return adjust( + ac.view(), + keylet, + args(A2.id(), 0, [&](Adjustements& sample) {})); + }, + XRPAmount{}, + STTx{ttVAULT_CLAWBACK, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp); + + // Not the same as above check: attempt to clawback MPT by bad account + doInvariantCheck( + {"clawback may only be performed by the asset issuer"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq() - 2); + return adjust( + ac.view(), + keylet, + args(A2.id(), 0, [&](Adjustements& sample) {})); + }, + XRPAmount{}, + STTx{ + ttVAULT_CLAWBACK, + [&](STObject& tx) { tx[sfAccount] = A4.id(); }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseMpt); + + doInvariantCheck( + {"clawback must decrease vault balance", + "clawback must decrease holder shares", + "clawback must change vault shares"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq() - 2); + return adjust( + ac.view(), + keylet, + args(A4.id(), 10, [&](Adjustements& sample) { + sample.sharesTotal = 0; + })); + }, + XRPAmount{}, + STTx{ + ttVAULT_CLAWBACK, + [&](STObject& tx) { + tx[sfAccount] = A3.id(); + tx[sfHolder] = A4.id(); + }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseMpt); + + doInvariantCheck( + {"clawback must change holder shares"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq() - 2); + return adjust( + ac.view(), + keylet, + args(A4.id(), -10, [&](Adjustements& sample) { + sample.accountShares->amount = 0; + })); + }, + XRPAmount{}, + STTx{ + ttVAULT_CLAWBACK, + [&](STObject& tx) { + tx[sfAccount] = A3.id(); + tx[sfHolder] = A4.id(); + }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseMpt); + + doInvariantCheck( + {"clawback must change holder and vault shares by equal amount", + "clawback and assets outstanding must add up", + "clawback and assets available must add up"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq() - 2); + return adjust( + ac.view(), + keylet, + args(A4.id(), -10, [&](Adjustements& sample) { + sample.accountShares->amount = -8; + sample.assetsTotal = -7; + sample.assetsAvailable = -7; + })); + }, + XRPAmount{}, + STTx{ + ttVAULT_CLAWBACK, + [&](STObject& tx) { + tx[sfAccount] = A3.id(); + tx[sfHolder] = A4.id(); + }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseMpt); + } + public: void run() override @@ -2169,6 +3868,7 @@ public: testNoModifiedUnmodifiableFields(); testValidPseudoAccounts(); testValidLoanBroker(); + testVault(); } }; diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index a293ded39c..b5f94f38d1 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -177,6 +177,14 @@ class Vault_test : public beast::unit_test::suite env.close(); } + { + testcase(prefix + " set maximum is idempotent, set it again"); + auto tx = vault.set({.owner = owner, .id = keylet.key}); + tx[sfAssetsMaximum] = asset(150).number(); + env(tx); + env.close(); + } + { testcase(prefix + " set data"); auto tx = vault.set({.owner = owner, .id = keylet.key}); @@ -218,6 +226,7 @@ class Vault_test : public beast::unit_test::suite .id = keylet.key, .amount = asset(1000)}); env(tx, ter(tecINSUFFICIENT_FUNDS)); + env.close(); } { @@ -385,6 +394,27 @@ class Vault_test : public beast::unit_test::suite env.balance(depositor, shares) == share(50 * scale)); } + if (!asset.raw().native()) + { + testcase(prefix + " issuer deposits"); + auto tx = vault.deposit( + {.depositor = issuer, + .id = keylet.key, + .amount = asset(10)}); + env(tx); + env.close(); + BEAST_EXPECT(env.balance(issuer, shares) == share(10 * scale)); + + testcase(prefix + " issuer withdraws"); + tx = vault.withdraw( + {.depositor = issuer, + .id = keylet.key, + .amount = share(10 * scale)}); + env(tx); + env.close(); + BEAST_EXPECT(env.balance(issuer, shares) == share(0 * scale)); + } + { testcase(prefix + " withdraw remaining assets"); auto tx = vault.withdraw( @@ -454,6 +484,8 @@ class Vault_test : public beast::unit_test::suite .amount = asset(10)}); tx[sfDestination] = erin.human(); env(tx); + env.close(); + // Erin returns assets to issuer env(pay(erin, issuer, asset(10))); env.close(); @@ -479,12 +511,14 @@ class Vault_test : public beast::unit_test::suite testcase(prefix + " fail to delete because wrong owner"); auto tx = vault.del({.owner = issuer, .id = keylet.key}); env(tx, ter(tecNO_PERMISSION)); + env.close(); } { testcase(prefix + " delete empty vault"); auto tx = vault.del({.owner = owner, .id = keylet.key}); env(tx); + env.close(); BEAST_EXPECT(!env.le(keylet)); } }; @@ -1363,6 +1397,26 @@ class Vault_test : public beast::unit_test::suite { using namespace test::jtx; { + { + testcase("IOU fail because MPT is disabled"); + Env env{ + *this, + (testable_amendments() - featureMPTokensV1) | + featureSingleAssetVault}; + Account issuer{"issuer"}; + Account owner{"owner"}; + env.fund(XRP(1000), issuer, owner); + env.close(); + + Vault vault{env}; + Asset asset = issuer["IOU"].asset(); + auto [tx, keylet] = + vault.create({.owner = owner, .asset = asset}); + + env(tx, ter(temDISABLED)); + env.close(); + } + { testcase("IOU fail create frozen"); Env env{*this, testable_amendments() | featureSingleAssetVault}; @@ -2913,6 +2967,12 @@ class Vault_test : public beast::unit_test::suite tx[sfDomainID] = to_string(domainId); env(tx); env.close(); + + // Should be idempotent + tx = vault.set({.owner = owner, .id = keylet.key}); + tx[sfDomainID] = to_string(domainId); + env(tx); + env.close(); } } @@ -3068,6 +3128,7 @@ class Vault_test : public beast::unit_test::suite .id = keylet.key, .amount = asset(50)}); env(tx); + env.close(); tx = vault.clawback( {.issuer = issuer, @@ -3082,6 +3143,7 @@ class Vault_test : public beast::unit_test::suite .holder = owner, .amount = asset(0)}); env(tx); + env.close(); tx = vault.del({ .owner = owner, @@ -3128,6 +3190,7 @@ class Vault_test : public beast::unit_test::suite auto tx = vault.deposit( {.depositor = owner, .id = keylet.key, .amount = asset(50)}); env(tx); + env.close(); } { diff --git a/src/test/rpc/Simulate_test.cpp b/src/test/rpc/Simulate_test.cpp index 0a36a8a841..5e7ab5be4d 100644 --- a/src/test/rpc/Simulate_test.cpp +++ b/src/test/rpc/Simulate_test.cpp @@ -136,11 +136,12 @@ class Simulate_test : public beast::unit_test::suite jtx::Env& env, Json::Value const& tx, std::function const& validate, Json::Value const& expectedMetadataKey, - bool testSerialized = true) + Json::Value const& expectedMetadataValue) { env.close(); @@ -149,8 +150,13 @@ class Simulate_test : public beast::unit_test::suite validate( env.rpc("json", "simulate", to_string(params)), tx, - expectedMetadataKey); - validate(env.rpc("simulate", to_string(tx)), tx, expectedMetadataKey); + expectedMetadataKey, + expectedMetadataValue); + validate( + env.rpc("simulate", to_string(tx)), + tx, + expectedMetadataKey, + expectedMetadataValue); BEAST_EXPECTS( env.current()->txCount() == 0, @@ -1218,73 +1224,93 @@ class Simulate_test : public beast::unit_test::suite testcase("Successful transaction with additional metadata"); using namespace jtx; + using namespace std::chrono_literals; Env env{*this, envconfig([&](std::unique_ptr cfg) { cfg->NETWORK_ID = 1025; return cfg; })}; Account const alice("alice"); + Account const bob("bob"); - env.fund(XRP(10000), alice); + env.fund(XRP(10000), alice, bob); env.close(); + // deliver_amount is unavailable in the metadata before 2014-02-01 + // so proceed to 2014-02-01 + env.close(NetClock::time_point{446000000s}); { - auto validateOutput = [&](Json::Value const& resp, - Json::Value const& tx, - Json::Value const& expectedMetadataKey) { - auto result = resp[jss::result]; + auto validateOutput = + [&](Json::Value const& resp, + Json::Value const& tx, + Json::Value const& expectedMetadataKey, + Json::Value const& expectedMetadataValue) { + auto result = resp[jss::result]; - BEAST_EXPECT(result[jss::engine_result] == "tesSUCCESS"); - BEAST_EXPECT(result[jss::engine_result_code] == 0); - BEAST_EXPECT( - result[jss::engine_result_message] == - "The simulated transaction would have been applied."); - - if (BEAST_EXPECT( - result.isMember(jss::meta) || - result.isMember(jss::meta_blob))) - { - Json::Value const metadata = getJsonMetadata(result); - - BEAST_EXPECT(metadata[sfTransactionIndex.jsonName] == 0); + BEAST_EXPECT(result[jss::engine_result] == "tesSUCCESS"); + BEAST_EXPECT(result[jss::engine_result_code] == 0); BEAST_EXPECT( - metadata[sfTransactionResult.jsonName] == "tesSUCCESS"); - BEAST_EXPECT( - metadata.isMember(expectedMetadataKey.asString())); - } - }; + result[jss::engine_result_message] == + "The simulated transaction would have been applied."); + + if (BEAST_EXPECT( + result.isMember(jss::meta) || + result.isMember(jss::meta_blob))) + { + Json::Value const metadata = getJsonMetadata(result); + + BEAST_EXPECT( + metadata[sfTransactionIndex.jsonName] == 0); + BEAST_EXPECT( + metadata[sfTransactionResult.jsonName] == + "tesSUCCESS"); + BEAST_EXPECT( + metadata.isMember(expectedMetadataKey.asString())); + BEAST_EXPECT( + metadata[expectedMetadataKey.asString()] == + expectedMetadataValue); + } + }; { Json::Value tx; - tx[jss::Account] = env.master.human(); + tx[jss::Account] = alice.human(); tx[jss::TransactionType] = jss::Payment; - tx[sfDestination] = alice.human(); + tx[sfDestination] = bob.human(); tx[sfAmount] = "100"; // test delivered amount testTxJsonMetadataField( - env, tx, validateOutput, jss::delivered_amount); + env, tx, validateOutput, jss::delivered_amount, "100"); } { Json::Value tx; - tx[jss::Account] = env.master.human(); + tx[jss::Account] = alice.human(); tx[jss::TransactionType] = jss::NFTokenMint; tx[sfNFTokenTaxon] = 1; + Json::Value nftokenId = + to_string(token::getNextID(env, alice, 1)); // test nft synthetic testTxJsonMetadataField( - env, tx, validateOutput, jss::nftoken_id); + env, tx, validateOutput, jss::nftoken_id, nftokenId); } { Json::Value tx; - tx[jss::Account] = env.master.human(); + tx[jss::Account] = alice.human(); tx[jss::TransactionType] = jss::MPTokenIssuanceCreate; + Json::Value mptIssuanceId = + to_string(makeMptID(env.seq(alice), alice)); // test mpt issuance id testTxJsonMetadataField( - env, tx, validateOutput, jss::mpt_issuance_id); + env, + tx, + validateOutput, + jss::mpt_issuance_id, + mptIssuanceId); } } } diff --git a/src/xrpld/app/ledger/Ledger.cpp b/src/xrpld/app/ledger/Ledger.cpp index 6de4f2cbde..64a6b16cbc 100644 --- a/src/xrpld/app/ledger/Ledger.cpp +++ b/src/xrpld/app/ledger/Ledger.cpp @@ -433,8 +433,10 @@ Ledger::read(Keylet const& k) const { if (k.key == beast::zero) { + // LCOV_EXCL_START UNREACHABLE("ripple::Ledger::read : zero key"); return nullptr; + // LCOV_EXCL_STOP } auto const& item = stateMap_.peekItem(k.key); if (!item) @@ -860,6 +862,7 @@ Ledger::assertSensible(beast::Journal ledgerJ) const return true; } + // LCOV_EXCL_START Json::Value j = getJson({*this, {}}); j[jss::accountTreeHash] = to_string(info_.accountHash); @@ -870,6 +873,7 @@ Ledger::assertSensible(beast::Journal ledgerJ) const UNREACHABLE("ripple::Ledger::assertSensible : ledger is not sensible"); return false; + // LCOV_EXCL_STOP } // update the skip list with the information from our previous ledger diff --git a/src/xrpld/app/ledger/detail/InboundLedger.cpp b/src/xrpld/app/ledger/detail/InboundLedger.cpp index eafa939506..47d546c3af 100644 --- a/src/xrpld/app/ledger/detail/InboundLedger.cpp +++ b/src/xrpld/app/ledger/detail/InboundLedger.cpp @@ -963,8 +963,10 @@ InboundLedger::takeAsRootNode(Slice const& data, SHAMapAddNode& san) if (!mHaveHeader) { + // LCOV_EXCL_START UNREACHABLE("ripple::InboundLedger::takeAsRootNode : no ledger header"); return false; + // LCOV_EXCL_STOP } AccountStateSF filter( @@ -988,8 +990,10 @@ InboundLedger::takeTxRootNode(Slice const& data, SHAMapAddNode& san) if (!mHaveHeader) { + // LCOV_EXCL_START UNREACHABLE("ripple::InboundLedger::takeTxRootNode : no ledger header"); return false; + // LCOV_EXCL_STOP } TransactionStateSF filter( diff --git a/src/xrpld/app/ledger/detail/LedgerMaster.cpp b/src/xrpld/app/ledger/detail/LedgerMaster.cpp index 78f0375b16..f39e8af0b6 100644 --- a/src/xrpld/app/ledger/detail/LedgerMaster.cpp +++ b/src/xrpld/app/ledger/detail/LedgerMaster.cpp @@ -1273,11 +1273,13 @@ LedgerMaster::findNewLedgersToPublish( } else if (hash->isZero()) { + // LCOV_EXCL_START JLOG(m_journal.fatal()) << "Ledger: " << valSeq << " does not have hash for " << seq; UNREACHABLE( "ripple::LedgerMaster::findNewLedgersToPublish : ledger " "not found"); + // LCOV_EXCL_STOP } else { diff --git a/src/xrpld/app/main/Application.cpp b/src/xrpld/app/main/Application.cpp index 05b8f5e5fa..616afc957d 100644 --- a/src/xrpld/app/main/Application.cpp +++ b/src/xrpld/app/main/Application.cpp @@ -1994,11 +1994,13 @@ ApplicationImp::loadOldLedger( if (!loadLedger) { + // LCOV_EXCL_START JLOG(m_journal.fatal()) << "Replay ledger missing/damaged"; UNREACHABLE( "ripple::ApplicationImp::loadOldLedger : replay ledger " "missing/damaged"); return false; + // LCOV_EXCL_STOP } } } @@ -2025,28 +2027,34 @@ ApplicationImp::loadOldLedger( if (loadLedger->info().accountHash.isZero()) { + // LCOV_EXCL_START JLOG(m_journal.fatal()) << "Ledger is empty."; UNREACHABLE( "ripple::ApplicationImp::loadOldLedger : ledger is empty"); return false; + // LCOV_EXCL_STOP } if (!loadLedger->walkLedger(journal("Ledger"), true)) { + // LCOV_EXCL_START JLOG(m_journal.fatal()) << "Ledger is missing nodes."; UNREACHABLE( "ripple::ApplicationImp::loadOldLedger : ledger is missing " "nodes"); return false; + // LCOV_EXCL_STOP } if (!loadLedger->assertSensible(journal("Ledger"))) { + // LCOV_EXCL_START JLOG(m_journal.fatal()) << "Ledger is not sensible."; UNREACHABLE( "ripple::ApplicationImp::loadOldLedger : ledger is not " "sensible"); return false; + // LCOV_EXCL_STOP } m_ledgerMaster->setLedgerRangePresent( diff --git a/src/xrpld/app/misc/NetworkOPs.cpp b/src/xrpld/app/misc/NetworkOPs.cpp index 3334052d4a..f7875b1bf3 100644 --- a/src/xrpld/app/misc/NetworkOPs.cpp +++ b/src/xrpld/app/misc/NetworkOPs.cpp @@ -1799,11 +1799,13 @@ NetworkOPsImp::getOwnerInfo( case ltACCOUNT_ROOT: case ltDIR_NODE: + // LCOV_EXCL_START default: UNREACHABLE( "ripple::NetworkOPsImp::getOwnerInfo : invalid " "type"); break; + // LCOV_EXCL_STOP } } @@ -3834,12 +3836,14 @@ NetworkOPsImp::addAccountHistoryJob(SubAccountHistoryInfoWeak subInfo) accountId, minLedger, maxLedger, marker, 0, true}; return db->newestAccountTxPage(options); } + // LCOV_EXCL_START default: { UNREACHABLE( "ripple::NetworkOPsImp::addAccountHistoryJob::" "getMoreTxns : invalid database type"); return {}; } + // LCOV_EXCL_STOP } }; @@ -4033,10 +4037,12 @@ NetworkOPsImp::subAccountHistoryStart( } else { + // LCOV_EXCL_START UNREACHABLE( "ripple::NetworkOPsImp::subAccountHistoryStart : failed to " "access genesis account"); return; + // LCOV_EXCL_STOP } } subInfo.index_->historyLastLedgerSeq_ = ledger->seq(); @@ -4143,7 +4149,11 @@ NetworkOPsImp::subBook(InfoSub::ref isrListener, Book const& book) if (auto listeners = app_.getOrderBookDB().makeBookListeners(book)) listeners->addSubscriber(isrListener); else + { + // LCOV_EXCL_START UNREACHABLE("ripple::NetworkOPsImp::subBook : null book listeners"); + // LCOV_EXCL_STOP + } return true; } diff --git a/src/xrpld/app/misc/detail/ValidatorList.cpp b/src/xrpld/app/misc/detail/ValidatorList.cpp index 2b45cec3be..92095b7211 100644 --- a/src/xrpld/app/misc/detail/ValidatorList.cpp +++ b/src/xrpld/app/misc/detail/ValidatorList.cpp @@ -1167,15 +1167,17 @@ ValidatorList::applyList( } if (!publicKeyType(*pubKeyOpt)) - { // LCOV_EXCL_START - // This is an impossible situation because we will never load an - // invalid public key type (see checks in `ValidatorList::load`) however - // we can only arrive here if the key used by the manifest matched one of - // the loaded keys + { + // This is an impossible situation because we will never load an + // invalid public key type (see checks in `ValidatorList::load`) however + // we can only arrive here if the key used by the manifest matched one + // of the loaded keys + // LCOV_EXCL_START UNREACHABLE( "ripple::ValidatorList::applyList : invalid public key type"); return PublisherListStats{result}; - } // LCOV_EXCL_STOP + // LCOV_EXCL_STOP + } PublicKey pubKey = *pubKeyOpt; if (result > ListDisposition::pending) diff --git a/src/xrpld/app/paths/Pathfinder.cpp b/src/xrpld/app/paths/Pathfinder.cpp index 41a3697888..4bc9304853 100644 --- a/src/xrpld/app/paths/Pathfinder.cpp +++ b/src/xrpld/app/paths/Pathfinder.cpp @@ -648,8 +648,10 @@ Pathfinder::getBestPaths( if (path.empty()) { + // LCOV_EXCL_START UNREACHABLE("ripple::Pathfinder::getBestPaths : path not found"); continue; + // LCOV_EXCL_STOP } bool startsWithIssuer = false; diff --git a/src/xrpld/app/paths/detail/BookStep.cpp b/src/xrpld/app/paths/detail/BookStep.cpp index 97cf87c046..54d0d8d0c9 100644 --- a/src/xrpld/app/paths/detail/BookStep.cpp +++ b/src/xrpld/app/paths/detail/BookStep.cpp @@ -1113,11 +1113,13 @@ BookStep::revImp( { case -1: { // something went very wrong + // LCOV_EXCL_START JLOG(j_.error()) << "BookStep remainingOut < 0 " << to_string(remainingOut); UNREACHABLE("ripple::BookStep::revImp : remaining less than zero"); cache_.emplace(beast::zero, beast::zero); return {beast::zero, beast::zero}; + // LCOV_EXCL_STOP } case 0: { // due to normalization, remainingOut can be zero without @@ -1283,12 +1285,14 @@ BookStep::fwdImp( switch (remainingIn.signum()) { case -1: { + // LCOV_EXCL_START // something went very wrong JLOG(j_.error()) << "BookStep remainingIn < 0 " << to_string(remainingIn); UNREACHABLE("ripple::BookStep::fwdImp : remaining less than zero"); cache_.emplace(beast::zero, beast::zero); return {beast::zero, beast::zero}; + // LCOV_EXCL_STOP } case 0: { // due to normalization, remainingIn can be zero without @@ -1421,8 +1425,10 @@ bookStepEqual(Step const& step, ripple::Book const& book) bool const outXRP = isXRP(book.out.currency); if (inXRP && outXRP) { + // LCOV_EXCL_START UNREACHABLE("ripple::test::bookStepEqual : no XRP to XRP book step"); return false; // no such thing as xrp/xrp book step + // LCOV_EXCL_STOP } if (inXRP && !outXRP) return equalHelper< diff --git a/src/xrpld/app/paths/detail/DirectStep.cpp b/src/xrpld/app/paths/detail/DirectStep.cpp index 03d207e008..a0808985b5 100644 --- a/src/xrpld/app/paths/detail/DirectStep.cpp +++ b/src/xrpld/app/paths/detail/DirectStep.cpp @@ -931,10 +931,12 @@ DirectStepI::check(StrandContext const& ctx) const { if (!ctx.prevStep) { + // LCOV_EXCL_START UNREACHABLE( "ripple::DirectStepI::check : prev seen book without a " "prev step"); return temBAD_PATH_LOOP; + // LCOV_EXCL_STOP } // This is OK if the previous step is a book step that outputs this diff --git a/src/xrpld/app/paths/detail/FlowDebugInfo.h b/src/xrpld/app/paths/detail/FlowDebugInfo.h index eec1d7c5a6..290a201e68 100644 --- a/src/xrpld/app/paths/detail/FlowDebugInfo.h +++ b/src/xrpld/app/paths/detail/FlowDebugInfo.h @@ -126,10 +126,12 @@ struct FlowDebugInfo auto i = timePoints.find(tag); if (i == timePoints.end()) { + // LCOV_EXCL_START UNREACHABLE( "ripple::path::detail::FlowDebugInfo::duration : timepoint not " "found"); return std::chrono::duration(0); + // LCOV_EXCL_STOP } auto const& t = i->second; return std::chrono::duration_cast>( diff --git a/src/xrpld/app/paths/detail/PaySteps.cpp b/src/xrpld/app/paths/detail/PaySteps.cpp index 6eb38eee83..f5621bcdf7 100644 --- a/src/xrpld/app/paths/detail/PaySteps.cpp +++ b/src/xrpld/app/paths/detail/PaySteps.cpp @@ -95,11 +95,13 @@ toStep( if (e1->isOffer() && e2->isAccount()) { + // LCOV_EXCL_START // should already be taken care of JLOG(j.error()) << "Found offer/account payment step. Aborting payment strand."; UNREACHABLE("ripple::toStep : offer/account payment payment strand"); return {temBAD_PATH, std::unique_ptr{}}; + // LCOV_EXCL_STOP } XRPL_ASSERT( @@ -392,8 +394,10 @@ toStrand( next->getCurrency() != curIssue.currency) { // Should never happen + // LCOV_EXCL_START UNREACHABLE("ripple::toStrand : offer currency mismatch"); return {temBAD_PATH, Strand{}}; + // LCOV_EXCL_STOP } auto s = toStep( @@ -457,9 +461,11 @@ toStrand( if (!checkStrand()) { + // LCOV_EXCL_START JLOG(j.warn()) << "Flow check strand failed"; UNREACHABLE("ripple::toStrand : invalid strand"); return {temBAD_PATH, Strand{}}; + // LCOV_EXCL_STOP } return {tesSUCCESS, std::move(result)}; diff --git a/src/xrpld/app/paths/detail/StrandFlow.h b/src/xrpld/app/paths/detail/StrandFlow.h index 47037c5f5e..7ccad9bf4f 100644 --- a/src/xrpld/app/paths/detail/StrandFlow.h +++ b/src/xrpld/app/paths/detail/StrandFlow.h @@ -167,6 +167,7 @@ flow( // Something is very wrong // throwing out the sandbox can only increase liquidity // yet the limiting is still limiting + // LCOV_EXCL_START JLOG(j.fatal()) << "Re-executed limiting step failed. r.first: " << to_string(get(r.first)) @@ -175,6 +176,7 @@ flow( "ripple::flow : first step re-executing the " "limiting step failed"); return Result{strand, std::move(ofrsToRm)}; + // LCOV_EXCL_STOP } } else if (!strand[i]->equalOut(r.second, stepOut)) @@ -202,6 +204,7 @@ flow( // Something is very wrong // throwing out the sandbox can only increase liquidity // yet the limiting is still limiting + // LCOV_EXCL_START #ifndef NDEBUG JLOG(j.fatal()) << "Re-executed limiting step failed. r.second: " @@ -213,6 +216,7 @@ flow( "ripple::flow : limiting step re-executing the " "limiting step failed"); return Result{strand, std::move(ofrsToRm)}; + // LCOV_EXCL_STOP } } @@ -238,6 +242,7 @@ flow( // The limits should already have been found, so executing a // strand forward from the limiting step should not find a // new limit + // LCOV_EXCL_START #ifndef NDEBUG JLOG(j.fatal()) << "Re-executed forward pass failed. r.first: " @@ -249,6 +254,7 @@ flow( "ripple::flow : non-limiting step re-executing the " "forward pass failed"); return Result{strand, std::move(ofrsToRm)}; + // LCOV_EXCL_STOP } stepIn = r.second; } @@ -499,8 +505,10 @@ public: { if (i >= cur_.size()) { + // LCOV_EXCL_START UNREACHABLE("ripple::ActiveStrands::get : input out of range"); return nullptr; + // LCOV_EXCL_STOP } return cur_[i]; } diff --git a/src/xrpld/app/rdb/RelationalDatabase.h b/src/xrpld/app/rdb/RelationalDatabase.h index 25b16f04a1..1a5b2ba830 100644 --- a/src/xrpld/app/rdb/RelationalDatabase.h +++ b/src/xrpld/app/rdb/RelationalDatabase.h @@ -235,12 +235,14 @@ rangeCheckedCast(C c) std::numeric_limits::is_signed && c < std::numeric_limits::lowest())) { - /* This should never happen */ + // This should never happen + // LCOV_EXCL_START UNREACHABLE("ripple::rangeCheckedCast : domain error"); JLOG(debugLog().error()) << "rangeCheckedCast domain error:" << " value = " << c << " min = " << std::numeric_limits::lowest() << " max: " << std::numeric_limits::max(); + // LCOV_EXCL_STOP } return static_cast(c); diff --git a/src/xrpld/app/rdb/backend/detail/Node.cpp b/src/xrpld/app/rdb/backend/detail/Node.cpp index 6a0544091b..88f11a272b 100644 --- a/src/xrpld/app/rdb/backend/detail/Node.cpp +++ b/src/xrpld/app/rdb/backend/detail/Node.cpp @@ -58,9 +58,11 @@ to_string(TableType type) return "Transactions"; case TableType::AccountTransactions: return "AccountTransactions"; + // LCOV_EXCL_START default: UNREACHABLE("ripple::detail::to_string : invalid TableType"); return "Unknown"; + // LCOV_EXCL_STOP } } @@ -202,18 +204,22 @@ saveValidatedLedger( if (!ledger->info().accountHash.isNonZero()) { + // LCOV_EXCL_START JLOG(j.fatal()) << "AH is zero: " << getJson({*ledger, {}}); UNREACHABLE("ripple::detail::saveValidatedLedger : zero account hash"); + // LCOV_EXCL_STOP } if (ledger->info().accountHash != ledger->stateMap().getHash().as_uint256()) { + // LCOV_EXCL_START JLOG(j.fatal()) << "sAL: " << ledger->info().accountHash << " != " << ledger->stateMap().getHash(); JLOG(j.fatal()) << "saveAcceptedLedger: seq=" << seq << ", current=" << current; UNREACHABLE( "ripple::detail::saveValidatedLedger : mismatched account hash"); + // LCOV_EXCL_STOP } XRPL_ASSERT( diff --git a/src/xrpld/app/tx/detail/Change.cpp b/src/xrpld/app/tx/detail/Change.cpp index aff76ba71a..f78eaf0f61 100644 --- a/src/xrpld/app/tx/detail/Change.cpp +++ b/src/xrpld/app/tx/detail/Change.cpp @@ -155,9 +155,11 @@ Change::doApply() return applyFee(); case ttUNL_MODIFY: return applyUNLModify(); + // LCOV_EXCL_START default: UNREACHABLE("ripple::Change::doApply : invalid transaction type"); return tefFAILURE; + // LCOV_EXCL_STOP } } diff --git a/src/xrpld/app/tx/detail/DeleteAccount.cpp b/src/xrpld/app/tx/detail/DeleteAccount.cpp index 565d938c83..d52e84d755 100644 --- a/src/xrpld/app/tx/detail/DeleteAccount.cpp +++ b/src/xrpld/app/tx/detail/DeleteAccount.cpp @@ -399,12 +399,14 @@ DeleteAccount::doApply() return {result, SkipEntry::No}; } + // LCOV_EXCL_START UNREACHABLE( "ripple::DeleteAccount::doApply : undeletable item not found " "in preclaim"); JLOG(j_.error()) << "DeleteAccount undeletable item not " "found in preclaim."; return {tecHAS_OBLIGATIONS, SkipEntry::No}; + // LCOV_EXCL_STOP }, ctx_.journal); if (ter != tesSUCCESS) diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index 20a647821f..3495cc3a02 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -24,17 +24,26 @@ #include #include +#include #include #include #include #include +#include +#include +#include +#include #include #include #include +#include #include #include #include +#include +#include + namespace ripple { /* @@ -78,6 +87,8 @@ enum Privilege { // 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 }; constexpr Privilege operator|(Privilege lhs, Privilege rhs) @@ -2482,4 +2493,941 @@ ValidLoan::finalize( 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.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), + "ripple::ValidVault::visitEntry : some object is available"); + + // `Number balance` will capture the difference (delta) between "before" + // state (zero if created) and "after" state (zero if destroyed), so the + // invariants can validate that the change in account balances matches the + // change in vault balances, stored to deltas_ at the end of this function. + Number balance{}; + + // By default do not add anything to deltas + 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)); + balance = static_cast( + before->getFieldU64(sfOutstandingAmount)); + sign = 1; + break; + case ltMPTOKEN: + balance = + static_cast(before->getFieldU64(sfMPTAmount)); + sign = -1; + break; + case ltACCOUNT_ROOT: + case ltRIPPLE_STATE: + balance = 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)); + balance -= Number(static_cast( + after->getFieldU64(sfOutstandingAmount))); + sign = 1; + break; + case ltMPTOKEN: + balance -= Number( + static_cast(after->getFieldU64(sfMPTAmount))); + sign = -1; + break; + case ltACCOUNT_ROOT: + case ltRIPPLE_STATE: + balance -= Number(after->getFieldAmount(sfBalance)); + sign = -1; + break; + default:; + } + } + + uint256 const key = (before ? before->key() : after->key()); + if (sign && balance != zero) + deltas_[key] = balance * 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, "ripple::ValidVault::finalize : vault noop invariant"); + return !enforce; + } + + return true; // Not a vault operation + } + else if (!hasPrivilege(tx, mustModifyVault)) // TODO: mayModifyVault + { + JLOG(j.fatal()) << // + "Invariant failed: vault updated by a wrong transaction type"; + XRPL_ASSERT( + enforce, + "ripple::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, "ripple::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, + "ripple::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, + "ripple::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, "ripple::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, + "ripple::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, + "ripple::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, "ripple::ValidVault::finalize : vault creation invariant"); + return !enforce; // That's all we can do here + } + + if (!beforeVault_.empty() && + afterVault.lossUnrealized != beforeVault_[0].lossUnrealized) + { + 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, "ripple::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 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; + }; + + // 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(), + "ripple::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(), + "ripple::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 = + [&]() -> std::optional { + if (auto ret = deltaAssets(tx[sfAccount]); ret) + { + // Compensate for transaction fee deduced from + // sfAccount + if (vaultAsset.native()) + *ret += fee.drops(); + if (*ret != zero) + return ret; + } + return std::nullopt; + }(); + + 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) + { + 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(), + "ripple::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 = + [&]() -> std::optional { + if (auto ret = deltaAssets(tx[sfAccount]); ret) + { + // Compensate for transaction fee deduced from + // sfAccount + if (vaultAsset.native()) + *ret += fee.drops(); + if (*ret != zero) + return ret; + } + return std::nullopt; + }(); + + auto const 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) + { + 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(), + "ripple::ValidVault::finalize : clawback updated a vault"); + auto const& beforeVault = beforeVault_[0]; + + if (vaultAsset.native() || + vaultAsset.getIssuer() != tx[sfAccount]) + { + JLOG(j.fatal()) << // + "Invariant failed: clawback may only be performed by " + "the asset issuer"; + return false; // That's all we can do + } + + auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId); + + if (!vaultDeltaAssets) + { + JLOG(j.fatal()) << // + "Invariant failed: clawback must change vault balance"; + return false; // That's all we can do + } + + if (*vaultDeltaAssets >= zero) + { + JLOG(j.fatal()) << // + "Invariant failed: clawback must decrease vault " + "balance"; + result = false; + } + + 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) + { + 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; + } + + 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; + } + + return result; + } + + default: + // LCOV_EXCL_START + UNREACHABLE( + "ripple::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, "ripple::ValidVault::finalize : vault invariants"); + return !enforce; + } + + return true; +} + } // namespace ripple diff --git a/src/xrpld/app/tx/detail/InvariantCheck.h b/src/xrpld/app/tx/detail/InvariantCheck.h index b2527c53e0..420bb646dd 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.h +++ b/src/xrpld/app/tx/detail/InvariantCheck.h @@ -20,8 +20,10 @@ #ifndef RIPPLE_APP_TX_INVARIANTCHECK_H_INCLUDED #define RIPPLE_APP_TX_INVARIANTCHECK_H_INCLUDED +#include #include #include +#include #include #include #include @@ -831,6 +833,74 @@ public: 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 = {}; + 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< @@ -856,7 +926,8 @@ using InvariantChecks = std::tuple< NoModifiedUnmodifiableFields, ValidPseudoAccounts, ValidLoanBroker, - ValidLoan>; + ValidLoan, + ValidVault>; /** * @brief get a tuple of all invariant checks diff --git a/src/xrpld/app/tx/detail/Offer.h b/src/xrpld/app/tx/detail/Offer.h index c214bea23f..58bd65ce46 100644 --- a/src/xrpld/app/tx/detail/Offer.h +++ b/src/xrpld/app/tx/detail/Offer.h @@ -225,11 +225,13 @@ template void TOffer::setFieldAmounts() { + // LCOV_EXCL_START #ifdef _MSC_VER UNREACHABLE("ripple::TOffer::setFieldAmounts : must be specialized"); #else static_assert(sizeof(TOut) == -1, "Must be specialized"); #endif + // LCOV_EXCL_STOP } template diff --git a/src/xrpld/app/tx/detail/OfferStream.cpp b/src/xrpld/app/tx/detail/OfferStream.cpp index 8e1215f5c8..9d43e419d7 100644 --- a/src/xrpld/app/tx/detail/OfferStream.cpp +++ b/src/xrpld/app/tx/detail/OfferStream.cpp @@ -369,10 +369,12 @@ TOfferStreamBase::step() std::is_same_v)) return shouldRmSmallIncreasedQOffer(); } + // LCOV_EXCL_START UNREACHABLE( "rippls::TOfferStreamBase::step::rmSmallIncreasedQOffer : XRP " "vs XRP offer"); return false; + // LCOV_EXCL_STOP }(); if (rmSmallIncreasedQOffer) diff --git a/src/xrpld/app/tx/detail/SetSignerList.cpp b/src/xrpld/app/tx/detail/SetSignerList.cpp index b5d9d4d5b8..ec2f902009 100644 --- a/src/xrpld/app/tx/detail/SetSignerList.cpp +++ b/src/xrpld/app/tx/detail/SetSignerList.cpp @@ -134,8 +134,10 @@ SetSignerList::doApply() default: break; } + // LCOV_EXCL_START UNREACHABLE("ripple::SetSignerList::doApply : invalid operation"); return temMALFORMED; + // LCOV_EXCL_STOP } void diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index b61680273a..5eb7eced72 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -1188,11 +1188,13 @@ Transactor::operator()() if (!s2.isEquivalent(ctx_.tx)) { + // LCOV_EXCL_START JLOG(j_.fatal()) << "Transaction serdes mismatch"; JLOG(j_.info()) << to_string(ctx_.tx.getJson(JsonOptions::none)); JLOG(j_.fatal()) << s2.getJson(JsonOptions::none); UNREACHABLE( "ripple::Transactor::operator() : transaction serdes mismatch"); + // LCOV_EXCL_STOP } } #endif diff --git a/src/xrpld/app/tx/detail/VaultDeposit.cpp b/src/xrpld/app/tx/detail/VaultDeposit.cpp index 75cf81b0b0..80382934ad 100644 --- a/src/xrpld/app/tx/detail/VaultDeposit.cpp +++ b/src/xrpld/app/tx/detail/VaultDeposit.cpp @@ -156,7 +156,10 @@ VaultDeposit::preclaim(PreclaimContext const& ctx) !isTesSuccess(ter)) return ter; - if (accountHolds( + // Asset issuer does not have any balance, they can just create funds by + // depositing in the vault. + if ((vaultAsset.native() || vaultAsset.getIssuer() != account) && + accountHolds( ctx.view, account, vaultAsset, diff --git a/src/xrpld/app/tx/detail/VaultSet.cpp b/src/xrpld/app/tx/detail/VaultSet.cpp index e9f7551154..eff47621fa 100644 --- a/src/xrpld/app/tx/detail/VaultSet.cpp +++ b/src/xrpld/app/tx/detail/VaultSet.cpp @@ -186,6 +186,9 @@ VaultSet::doApply() view().update(sleIssuance); } + // Note, we must update Vault object even if only DomainID is being updated + // in Issuance object. Otherwise it's really difficult for Vault invariants + // to verify the operation. view().update(vault); return tesSUCCESS; diff --git a/src/xrpld/app/tx/detail/XChainBridge.cpp b/src/xrpld/app/tx/detail/XChainBridge.cpp index 2587845df5..5f5c081e2f 100644 --- a/src/xrpld/app/tx/detail/XChainBridge.cpp +++ b/src/xrpld/app/tx/detail/XChainBridge.cpp @@ -223,10 +223,12 @@ claimHelper( auto i = signersList.find(a.keyAccount); if (i == signersList.end()) { + // LCOV_EXCL_START UNREACHABLE( "ripple::claimHelper : invalid inputs"); // should have already // been checked continue; + // LCOV_EXCL_STOP } weight += i->second; rewardAccounts.push_back(a.rewardAccount); diff --git a/src/xrpld/app/tx/detail/applySteps.cpp b/src/xrpld/app/tx/detail/applySteps.cpp index c2e4e13f08..1cad93eedc 100644 --- a/src/xrpld/app/tx/detail/applySteps.cpp +++ b/src/xrpld/app/tx/detail/applySteps.cpp @@ -129,10 +129,12 @@ invoke_preflight(PreflightContext const& ctx) catch (UnknownTxnType const& e) { // Should never happen + // LCOV_EXCL_START JLOG(ctx.j.fatal()) << "Unknown transaction type in preflight: " << e.txnType; UNREACHABLE("ripple::invoke_preflight : unknown transaction type"); return {temUNKNOWN, TxConsequences{temUNKNOWN}}; + // LCOV_EXCL_STOP } } @@ -183,10 +185,12 @@ invoke_preclaim(PreclaimContext const& ctx) catch (UnknownTxnType const& e) { // Should never happen + // LCOV_EXCL_START JLOG(ctx.j.fatal()) << "Unknown transaction type in preclaim: " << e.txnType; UNREACHABLE("ripple::invoke_preclaim : unknown transaction type"); return temUNKNOWN; + // LCOV_EXCL_STOP } } @@ -217,9 +221,11 @@ invoke_calculateBaseFee(ReadView const& view, STTx const& tx) } catch (UnknownTxnType const& e) { + // LCOV_EXCL_START UNREACHABLE( "ripple::invoke_calculateBaseFee : unknown transaction type"); return XRPAmount{0}; + // LCOV_EXCL_STOP } } @@ -277,10 +283,12 @@ invoke_apply(ApplyContext& ctx) catch (UnknownTxnType const& e) { // Should never happen + // LCOV_EXCL_START JLOG(ctx.journal.fatal()) << "Unknown transaction type in apply: " << e.txnType; UNREACHABLE("ripple::invoke_apply : unknown transaction type"); return {temUNKNOWN, false}; + // LCOV_EXCL_STOP } } diff --git a/src/xrpld/nodestore/backend/NuDBFactory.cpp b/src/xrpld/nodestore/backend/NuDBFactory.cpp index 2f4e9d502e..727dec6f3e 100644 --- a/src/xrpld/nodestore/backend/NuDBFactory.cpp +++ b/src/xrpld/nodestore/backend/NuDBFactory.cpp @@ -121,11 +121,13 @@ public: using namespace boost::filesystem; if (db_.is_open()) { + // LCOV_EXCL_START UNREACHABLE( "ripple::NodeStore::NuDBBackend::open : database is already " "open"); JLOG(j_.error()) << "database is already open"; return; + // LCOV_EXCL_STOP } auto const folder = path(name_); auto const dp = (folder / "nudb.dat").string(); diff --git a/src/xrpld/nodestore/backend/RocksDBFactory.cpp b/src/xrpld/nodestore/backend/RocksDBFactory.cpp index 0e421cd6bd..57c136a10a 100644 --- a/src/xrpld/nodestore/backend/RocksDBFactory.cpp +++ b/src/xrpld/nodestore/backend/RocksDBFactory.cpp @@ -232,11 +232,13 @@ public: { if (m_db) { + // LCOV_EXCL_START UNREACHABLE( "ripple::NodeStore::RocksDBBackend::open : database is already " "open"); JLOG(m_journal.error()) << "database is already open"; return; + // LCOV_EXCL_STOP } rocksdb::DB* db = nullptr; m_options.create_if_missing = createIfMissing; diff --git a/src/xrpld/overlay/Compression.h b/src/xrpld/overlay/Compression.h index 3a278a3403..35af4bf925 100644 --- a/src/xrpld/overlay/Compression.h +++ b/src/xrpld/overlay/Compression.h @@ -60,12 +60,14 @@ decompress( in, inSize, decompressed, decompressedSize); else { + // LCOV_EXCL_START JLOG(debugLog().warn()) << "decompress: invalid compression algorithm " << static_cast(algorithm); UNREACHABLE( "ripple::compression::decompress : invalid compression " "algorithm"); + // LCOV_EXCL_STOP } } catch (...) @@ -98,11 +100,13 @@ compress( in, inSize, std::forward(bf)); else { + // LCOV_EXCL_START JLOG(debugLog().warn()) << "compress: invalid compression algorithm" << static_cast(algorithm); UNREACHABLE( "ripple::compression::compress : invalid compression " "algorithm"); + // LCOV_EXCL_STOP } } catch (...) diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index 127ab4655e..fe07983f89 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -2249,10 +2249,12 @@ PeerImp::onValidatorListMessage( case ListDisposition::invalid: case ListDisposition::unsupported_version: break; + // LCOV_EXCL_START default: UNREACHABLE( "ripple::PeerImp::onValidatorListMessage : invalid best list " "disposition"); + // LCOV_EXCL_STOP } // Charge based on the worst result @@ -2293,10 +2295,12 @@ PeerImp::onValidatorListMessage( // If it happens frequently, that's probably bad. fee_.update(Resource::feeInvalidData, "version"); break; + // LCOV_EXCL_START default: UNREACHABLE( "ripple::PeerImp::onValidatorListMessage : invalid worst list " "disposition"); + // LCOV_EXCL_STOP } // Log based on all the results. @@ -2353,10 +2357,12 @@ PeerImp::onValidatorListMessage( << "Ignored " << count << "invalid " << messageType << "(s) from peer " << remote_address_; break; + // LCOV_EXCL_START default: UNREACHABLE( "ripple::PeerImp::onValidatorListMessage : invalid list " "disposition"); + // LCOV_EXCL_STOP } } } diff --git a/src/xrpld/peerfinder/detail/Counts.h b/src/xrpld/peerfinder/detail/Counts.h index 821431c5bb..a35473ddb5 100644 --- a/src/xrpld/peerfinder/detail/Counts.h +++ b/src/xrpld/peerfinder/detail/Counts.h @@ -295,10 +295,12 @@ private: m_closingCount += n; break; + // LCOV_EXCL_START default: UNREACHABLE( "ripple::PeerFinder::Counts::adjust : invalid input state"); break; + // LCOV_EXCL_STOP }; } diff --git a/src/xrpld/peerfinder/detail/Logic.h b/src/xrpld/peerfinder/detail/Logic.h index 4b92a1d143..74bec8431e 100644 --- a/src/xrpld/peerfinder/detail/Logic.h +++ b/src/xrpld/peerfinder/detail/Logic.h @@ -976,11 +976,13 @@ public: << slot->remote_endpoint(); break; + // LCOV_EXCL_START default: UNREACHABLE( "ripple::PeerFinder::Logic::on_closed : invalid slot " "state"); break; + // LCOV_EXCL_STOP } } diff --git a/src/xrpld/perflog/detail/PerfLogImp.cpp b/src/xrpld/perflog/detail/PerfLogImp.cpp index a34faf885c..583ec095e1 100644 --- a/src/xrpld/perflog/detail/PerfLogImp.cpp +++ b/src/xrpld/perflog/detail/PerfLogImp.cpp @@ -53,9 +53,11 @@ PerfLogImp::Counters::Counters( if (!inserted) { // Ensure that no other function populates this entry. + // LCOV_EXCL_START UNREACHABLE( "ripple::perf::PerfLogImp::Counters::Counters : failed to " "insert label"); + // LCOV_EXCL_STOP } } } @@ -68,9 +70,11 @@ PerfLogImp::Counters::Counters( if (!inserted) { // Ensure that no other function populates this entry. + // LCOV_EXCL_START UNREACHABLE( "ripple::perf::PerfLogImp::Counters::Counters : failed to " "insert job type"); + // LCOV_EXCL_STOP } } } @@ -329,8 +333,10 @@ PerfLogImp::rpcStart(std::string const& method, std::uint64_t const requestId) auto counter = counters_.rpc_.find(method); if (counter == counters_.rpc_.end()) { + // LCOV_EXCL_START UNREACHABLE("ripple::perf::PerfLogImp::rpcStart : valid method input"); return; + // LCOV_EXCL_STOP } { @@ -351,8 +357,10 @@ PerfLogImp::rpcEnd( auto counter = counters_.rpc_.find(method); if (counter == counters_.rpc_.end()) { + // LCOV_EXCL_START UNREACHABLE("ripple::perf::PerfLogImp::rpcEnd : valid method input"); return; + // LCOV_EXCL_STOP } steady_time_point startTime; { @@ -365,8 +373,10 @@ PerfLogImp::rpcEnd( } else { + // LCOV_EXCL_START UNREACHABLE( "ripple::perf::PerfLogImp::rpcEnd : valid requestId input"); + // LCOV_EXCL_STOP } } std::lock_guard lock(counter->second.mutex); @@ -384,9 +394,11 @@ PerfLogImp::jobQueue(JobType const type) auto counter = counters_.jq_.find(type); if (counter == counters_.jq_.end()) { + // LCOV_EXCL_START UNREACHABLE( "ripple::perf::PerfLogImp::jobQueue : valid job type input"); return; + // LCOV_EXCL_STOP } std::lock_guard lock(counter->second.mutex); ++counter->second.value.queued; @@ -402,10 +414,13 @@ PerfLogImp::jobStart( auto counter = counters_.jq_.find(type); if (counter == counters_.jq_.end()) { + // LCOV_EXCL_START UNREACHABLE( "ripple::perf::PerfLogImp::jobStart : valid job type input"); return; + // LCOV_EXCL_STOP } + { std::lock_guard lock(counter->second.mutex); ++counter->second.value.started; @@ -422,10 +437,13 @@ PerfLogImp::jobFinish(JobType const type, microseconds dur, int instance) auto counter = counters_.jq_.find(type); if (counter == counters_.jq_.end()) { + // LCOV_EXCL_START UNREACHABLE( "ripple::perf::PerfLogImp::jobFinish : valid job type input"); return; + // LCOV_EXCL_STOP } + { std::lock_guard lock(counter->second.mutex); ++counter->second.value.finished; diff --git a/src/xrpld/rpc/detail/Handler.cpp b/src/xrpld/rpc/detail/Handler.cpp index 3b32524ee2..d15c5aaed0 100644 --- a/src/xrpld/rpc/detail/Handler.cpp +++ b/src/xrpld/rpc/detail/Handler.cpp @@ -39,8 +39,10 @@ byRef(Function const& f) result = f(context); if (result.type() != Json::objectValue) { + // LCOV_EXCL_START UNREACHABLE("ripple::RPC::byRef : result is object"); result = RPC::makeObjectValue(result); + // LCOV_EXCL_STOP } return Status(); diff --git a/src/xrpld/rpc/detail/Status.cpp b/src/xrpld/rpc/detail/Status.cpp index 738219b6b5..ce15003968 100644 --- a/src/xrpld/rpc/detail/Status.cpp +++ b/src/xrpld/rpc/detail/Status.cpp @@ -51,8 +51,10 @@ Status::codeString() const return sStr.str(); } + // LCOV_EXCL_START UNREACHABLE("ripple::RPC::codeString : invalid type"); return ""; + // LCOV_EXCL_STOP } void diff --git a/src/xrpld/rpc/handlers/AccountChannels.cpp b/src/xrpld/rpc/handlers/AccountChannels.cpp index 1b0046ab64..17e46f052f 100644 --- a/src/xrpld/rpc/handlers/AccountChannels.cpp +++ b/src/xrpld/rpc/handlers/AccountChannels.cpp @@ -169,8 +169,10 @@ doAccountChannels(RPC::JsonContext& context) std::shared_ptr const& sleCur) { if (!sleCur) { + // LCOV_EXCL_START UNREACHABLE("ripple::doAccountChannels : null SLE"); return false; + // LCOV_EXCL_STOP } if (++count == limit) diff --git a/src/xrpld/rpc/handlers/AccountLines.cpp b/src/xrpld/rpc/handlers/AccountLines.cpp index 893ca9a190..146a9527a9 100644 --- a/src/xrpld/rpc/handlers/AccountLines.cpp +++ b/src/xrpld/rpc/handlers/AccountLines.cpp @@ -193,8 +193,10 @@ doAccountLines(RPC::JsonContext& context) std::shared_ptr const& sleCur) { if (!sleCur) { + // LCOV_EXCL_START UNREACHABLE("ripple::doAccountLines : null SLE"); return false; + // LCOV_EXCL_STOP } if (++count == limit) diff --git a/src/xrpld/rpc/handlers/AccountOffers.cpp b/src/xrpld/rpc/handlers/AccountOffers.cpp index e65b39b35b..1f2b76efe4 100644 --- a/src/xrpld/rpc/handlers/AccountOffers.cpp +++ b/src/xrpld/rpc/handlers/AccountOffers.cpp @@ -145,8 +145,10 @@ doAccountOffers(RPC::JsonContext& context) std::shared_ptr const& sle) { if (!sle) { + // LCOV_EXCL_START UNREACHABLE("ripple::doAccountOffers : null SLE"); return false; + // LCOV_EXCL_STOP } if (++count == limit) diff --git a/src/xrpld/rpc/handlers/AccountTx.cpp b/src/xrpld/rpc/handlers/AccountTx.cpp index 6b1dccdba9..e053c2adc0 100644 --- a/src/xrpld/rpc/handlers/AccountTx.cpp +++ b/src/xrpld/rpc/handlers/AccountTx.cpp @@ -353,9 +353,13 @@ populateJsonResponse( jvObj[jss::meta], sttx, *txnMeta); } else + { + // LCOV_EXCL_START UNREACHABLE( "ripple::populateJsonResponse : missing " "transaction medatata"); + // LCOV_EXCL_STOP + } } } } diff --git a/src/xrpld/rpc/handlers/Fee1.cpp b/src/xrpld/rpc/handlers/Fee1.cpp index 6d15a4d95f..ecb4ad4b29 100644 --- a/src/xrpld/rpc/handlers/Fee1.cpp +++ b/src/xrpld/rpc/handlers/Fee1.cpp @@ -32,9 +32,12 @@ doFee(RPC::JsonContext& context) auto result = context.app.getTxQ().doRPC(context.app); if (result.type() == Json::objectValue) return result; + + // LCOV_EXCL_START UNREACHABLE("ripple::doFee : invalid result type"); RPC::inject_error(rpcINTERNAL, context.params); return context.params; + // LCOV_EXCL_STOP } } // namespace ripple diff --git a/src/xrpld/shamap/detail/SHAMap.cpp b/src/xrpld/shamap/detail/SHAMap.cpp index d2415a2ff2..026149be56 100644 --- a/src/xrpld/shamap/detail/SHAMap.cpp +++ b/src/xrpld/shamap/detail/SHAMap.cpp @@ -545,8 +545,10 @@ SHAMap::onlyBelow(SHAMapTreeNode* node) const if (!nextNode) { + // LCOV_EXCL_START UNREACHABLE("ripple::SHAMap::onlyBelow : no next node"); return no_item; + // LCOV_EXCL_STOP } node = nextNode; @@ -922,8 +924,10 @@ SHAMap::updateGiveItem( if (!node || (node->peekItem()->key() != tag)) { + // LCOV_EXCL_START UNREACHABLE("ripple::SHAMap::updateGiveItem : invalid node"); return false; + // LCOV_EXCL_STOP } if (node->getType() != type) diff --git a/src/xrpld/shamap/detail/SHAMapDelta.cpp b/src/xrpld/shamap/detail/SHAMapDelta.cpp index 2adce62efc..ebdaffad14 100644 --- a/src/xrpld/shamap/detail/SHAMapDelta.cpp +++ b/src/xrpld/shamap/detail/SHAMapDelta.cpp @@ -149,8 +149,10 @@ SHAMap::compare(SHAMap const& otherMap, Delta& differences, int maxCount) const if (!ourNode || !otherNode) { + // LCOV_EXCL_START UNREACHABLE("ripple::SHAMap::compare : missing a node"); Throw(type_, uint256()); + // LCOV_EXCL_STOP } if (ourNode->isLeaf() && otherNode->isLeaf()) @@ -230,7 +232,11 @@ SHAMap::compare(SHAMap const& otherMap, Delta& differences, int maxCount) const } } else + { + // LCOV_EXCL_START UNREACHABLE("ripple::SHAMap::compare : invalid node"); + // LCOV_EXCL_STOP + } } return true;