From 2df730438db77df3c9d40a0e50ce78f62f532568 Mon Sep 17 00:00:00 2001 From: Bart Date: Tue, 7 Oct 2025 16:28:19 -0400 Subject: [PATCH 1/7] Set version to 3.0.0-b1 (#5859) --- src/libxrpl/protocol/BuildInfo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) From 176fd2b6e4486198b463d9f9b87375d069b9b7cc Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Wed, 8 Oct 2025 04:25:51 -0400 Subject: [PATCH 2/7] chore: exclude all `UNREACHABLE` blocks from codecov (#5846) --- include/xrpl/basics/IntrusivePointer.ipp | 2 ++ include/xrpl/beast/net/IPAddress.h | 4 ++++ include/xrpl/ledger/ApplyView.h | 2 ++ include/xrpl/protocol/detail/b58_utils.h | 2 ++ include/xrpl/resource/detail/Logic.h | 2 ++ src/libxrpl/basics/Log.cpp | 8 +++++++ src/libxrpl/basics/contract.cpp | 2 ++ src/libxrpl/json/Object.cpp | 4 ++-- src/libxrpl/json/json_value.cpp | 24 +++++++++++++++++++ src/libxrpl/ledger/ApplyStateTable.cpp | 2 ++ src/libxrpl/ledger/ApplyView.cpp | 2 ++ src/libxrpl/ledger/BookDirs.cpp | 4 ++++ src/libxrpl/ledger/View.cpp | 12 ++++++---- src/libxrpl/protocol/STBase.cpp | 2 ++ src/libxrpl/protocol/TxMeta.cpp | 2 ++ src/xrpld/app/ledger/Ledger.cpp | 4 ++++ src/xrpld/app/ledger/detail/InboundLedger.cpp | 4 ++++ src/xrpld/app/ledger/detail/LedgerMaster.cpp | 2 ++ src/xrpld/app/main/Application.cpp | 8 +++++++ src/xrpld/app/misc/NetworkOPs.cpp | 10 ++++++++ src/xrpld/app/misc/detail/ValidatorList.cpp | 14 ++++++----- src/xrpld/app/paths/Pathfinder.cpp | 2 ++ src/xrpld/app/paths/detail/BookStep.cpp | 6 +++++ src/xrpld/app/paths/detail/DirectStep.cpp | 2 ++ src/xrpld/app/paths/detail/FlowDebugInfo.h | 2 ++ src/xrpld/app/paths/detail/PaySteps.cpp | 6 +++++ src/xrpld/app/paths/detail/StrandFlow.h | 8 +++++++ src/xrpld/app/rdb/RelationalDatabase.h | 4 +++- src/xrpld/app/rdb/backend/detail/Node.cpp | 6 +++++ src/xrpld/app/tx/detail/Change.cpp | 2 ++ src/xrpld/app/tx/detail/DeleteAccount.cpp | 2 ++ src/xrpld/app/tx/detail/Offer.h | 2 ++ src/xrpld/app/tx/detail/OfferStream.cpp | 2 ++ src/xrpld/app/tx/detail/SetSignerList.cpp | 2 ++ src/xrpld/app/tx/detail/Transactor.cpp | 2 ++ src/xrpld/app/tx/detail/XChainBridge.cpp | 2 ++ src/xrpld/app/tx/detail/applySteps.cpp | 8 +++++++ src/xrpld/nodestore/backend/NuDBFactory.cpp | 2 ++ .../nodestore/backend/RocksDBFactory.cpp | 2 ++ src/xrpld/overlay/Compression.h | 4 ++++ src/xrpld/overlay/detail/PeerImp.cpp | 6 +++++ src/xrpld/peerfinder/detail/Counts.h | 2 ++ src/xrpld/peerfinder/detail/Logic.h | 2 ++ src/xrpld/perflog/detail/PerfLogImp.cpp | 18 ++++++++++++++ src/xrpld/rpc/detail/Handler.cpp | 2 ++ src/xrpld/rpc/detail/Status.cpp | 2 ++ src/xrpld/rpc/handlers/AccountChannels.cpp | 2 ++ src/xrpld/rpc/handlers/AccountLines.cpp | 2 ++ src/xrpld/rpc/handlers/AccountOffers.cpp | 2 ++ src/xrpld/rpc/handlers/AccountTx.cpp | 4 ++++ src/xrpld/rpc/handlers/Fee1.cpp | 3 +++ src/xrpld/shamap/detail/SHAMap.cpp | 4 ++++ src/xrpld/shamap/detail/SHAMapDelta.cpp | 6 +++++ 53 files changed, 224 insertions(+), 13 deletions(-) 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 d8b9028d7c..f90033966a 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/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 8a0fd51bd8..b2a24f4582 100644 --- a/src/libxrpl/ledger/ApplyView.cpp +++ b/src/libxrpl/ledger/ApplyView.cpp @@ -133,8 +133,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 10b7e81b4f..02de872ad3 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); } @@ -2676,7 +2679,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/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/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 b9069442f8..e72b2732d0 100644 --- a/src/xrpld/app/misc/NetworkOPs.cpp +++ b/src/xrpld/app/misc/NetworkOPs.cpp @@ -1798,11 +1798,13 @@ NetworkOPsImp::getOwnerInfo( case ltACCOUNT_ROOT: case ltDIR_NODE: + // LCOV_EXCL_START default: UNREACHABLE( "ripple::NetworkOPsImp::getOwnerInfo : invalid " "type"); break; + // LCOV_EXCL_STOP } } @@ -3831,12 +3833,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 } }; @@ -4030,10 +4034,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(); @@ -4140,7 +4146,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 d6a31024f3..77d098ea0f 100644 --- a/src/xrpld/app/tx/detail/Change.cpp +++ b/src/xrpld/app/tx/detail/Change.cpp @@ -151,9 +151,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/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 f6b8b3c9d2..920b1a58bc 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -1170,11 +1170,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/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 93371f42ab..47d01eb7c5 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -2234,10 +2234,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 @@ -2278,10 +2280,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. @@ -2338,10 +2342,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; From f61086b43c93322c73738a4f30efb54d986070d7 Mon Sep 17 00:00:00 2001 From: Bart Date: Wed, 8 Oct 2025 09:15:24 -0400 Subject: [PATCH 3/7] refactor: Update CI strategy matrix to use new RHEL 9 and RHEL 10 images (#5856) This change uses the new RHEL 9 and 10 images to build and test the binary, and adds support for having different Docker image SHAs per distro-compiler combination. Instead of supporting RHEL each minor version, we are simplifying our pipelines by only supporting RHEL major versions. Our CI Docker images have already been updated accordingly, and we recently added support for RHEL 10 as well. Up until now, the CI Docker images had all been rebuilt at the same time, but that is not necessarily true as the most recent push to the CI repo has shown where the RHEL images now have a different SHA than the Debian and Ubuntu ones. Co-authored-by: Bart Thomee <11445373+bthomee@users.noreply.github.com> --- .github/scripts/strategy-matrix/generate.py | 8 +- .github/scripts/strategy-matrix/linux.json | 92 ++++++++++++-------- .github/scripts/strategy-matrix/macos.json | 3 +- .github/scripts/strategy-matrix/windows.json | 3 +- .github/workflows/reusable-build-test.yml | 2 +- .github/workflows/upload-conan-deps.yml | 5 +- 6 files changed, 66 insertions(+), 47 deletions(-) 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' }} From 6b6b213cf543d98eb76af7ac4a6a1379ce26ca4e Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Wed, 8 Oct 2025 14:45:44 +0100 Subject: [PATCH 4/7] chore: Fix release build error (#5864) This change fixes a release build error with GCC 15.2. The `fields` variable is only used in `XRPL_ASSERT`, which evaluates to nothing in a Release build, leaving the variable unused. This change silences the build warning. Co-authored-by: Bart Thomee <11445373+bthomee@users.noreply.github.com> --- src/libxrpl/ledger/View.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index 02de872ad3..89d8137ac7 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -1134,7 +1134,7 @@ createPseudoAccount( uint256 const& pseudoOwnerKey, SField const& ownerField) { - auto const& fields = getPseudoAccountFields(); + [[maybe_unused]] auto const& fields = getPseudoAccountFields(); XRPL_ASSERT( std::count_if( fields.begin(), From 620fb26823777aedd62fb179f3d39078ac66c5cd Mon Sep 17 00:00:00 2001 From: tequ Date: Wed, 8 Oct 2025 23:36:09 +0900 Subject: [PATCH 5/7] test: Add more tests for Simulate RPC metadata (#5827) --- src/test/rpc/Simulate_test.cpp | 92 ++++++++++++++++++++++------------ 1 file changed, 59 insertions(+), 33 deletions(-) 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); } } } From 5ecde3cf39dcb784250253e9d953e9a4d4d2bb34 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Wed, 8 Oct 2025 16:04:02 +0100 Subject: [PATCH 6/7] Add vault invariants (#5518) This change adds invariants for SingleAssetVault #5224 (XLS-065), which had been intentionally skipped earlier to keep the SAV PR size manageable. --- .../xrpl/protocol/detail/transactions.macro | 12 +- src/test/app/Invariants_test.cpp | 1706 ++++++++++++++++- src/test/app/Vault_test.cpp | 63 + src/xrpld/app/tx/detail/InvariantCheck.cpp | 950 +++++++++ src/xrpld/app/tx/detail/InvariantCheck.h | 73 +- src/xrpld/app/tx/detail/VaultDeposit.cpp | 5 +- src/xrpld/app/tx/detail/VaultSet.cpp | 3 + 7 files changed, 2801 insertions(+), 11 deletions(-) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 3ea4a3bbec..2565372ebc 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/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index c91149b2f7..8781eee72b 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(); } } @@ -1726,6 +1741,1690 @@ class Invariants_test : public beast::unit_test::suite {tecINVARIANT_FAILED, tecINVARIANT_FAILED}); } + 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 @@ -1746,6 +3445,7 @@ public: testPermissionedDomainInvariants(); testValidPseudoAccounts(); testPermissionedDEX(); + testVault(); } }; diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 159bfd0796..4a731ddd57 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)); } }; @@ -1328,6 +1362,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}; @@ -2878,6 +2932,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(); } } @@ -3033,6 +3093,7 @@ class Vault_test : public beast::unit_test::suite .id = keylet.key, .amount = asset(50)}); env(tx); + env.close(); tx = vault.clawback( {.issuer = issuer, @@ -3047,6 +3108,7 @@ class Vault_test : public beast::unit_test::suite .holder = owner, .amount = asset(0)}); env(tx); + env.close(); tx = vault.del({ .owner = owner, @@ -3093,6 +3155,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/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index 87ca9ea6c1..2cfcb6d258 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) @@ -2170,4 +2181,943 @@ ValidAMM::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 5444f2f3a9..97d51f0fab 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 @@ -732,6 +734,74 @@ private: beast::Journal const&) 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< @@ -754,7 +824,8 @@ using InvariantChecks = std::tuple< ValidPermissionedDomain, ValidPermissionedDEX, ValidAMM, - ValidPseudoAccounts>; + ValidPseudoAccounts, + ValidVault>; /** * @brief get a tuple of all invariant checks 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 6057e40cfa..170a850a36 100644 --- a/src/xrpld/app/tx/detail/VaultSet.cpp +++ b/src/xrpld/app/tx/detail/VaultSet.cpp @@ -183,6 +183,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; From 46ba8a28fecd0665900e3a1caf276a426ad6640a Mon Sep 17 00:00:00 2001 From: Bart Date: Thu, 9 Oct 2025 13:27:26 -0400 Subject: [PATCH 7/7] refactor: Update Conan dependencies: OpenSSL (#5873) This change bumps OpenSSL from 1.1.1w to 3.6.0. --- .github/scripts/strategy-matrix/linux.json | 12 ++++++------ cmake/RippledCompiler.cmake | 17 ++++++++++------- conan.lock | 2 +- conanfile.py | 2 +- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/.github/scripts/strategy-matrix/linux.json b/.github/scripts/strategy-matrix/linux.json index 08313daf0a..bae5c57087 100644 --- a/.github/scripts/strategy-matrix/linux.json +++ b/.github/scripts/strategy-matrix/linux.json @@ -78,42 +78,42 @@ "distro_version": "9", "compiler_name": "gcc", "compiler_version": "12", - "image_sha": "0ab1e4c" + "image_sha": "d133ce3" }, { "distro_name": "rhel", "distro_version": "9", "compiler_name": "gcc", "compiler_version": "13", - "image_sha": "0ab1e4c" + "image_sha": "d133ce3" }, { "distro_name": "rhel", "distro_version": "9", "compiler_name": "gcc", "compiler_version": "14", - "image_sha": "0ab1e4c" + "image_sha": "d133ce3" }, { "distro_name": "rhel", "distro_version": "9", "compiler_name": "clang", "compiler_version": "any", - "image_sha": "0ab1e4c" + "image_sha": "d133ce3" }, { "distro_name": "rhel", "distro_version": "10", "compiler_name": "gcc", "compiler_version": "14", - "image_sha": "0ab1e4c" + "image_sha": "d133ce3" }, { "distro_name": "rhel", "distro_version": "10", "compiler_name": "clang", "compiler_version": "any", - "image_sha": "0ab1e4c" + "image_sha": "d133ce3" }, { "distro_name": "ubuntu", diff --git a/cmake/RippledCompiler.cmake b/cmake/RippledCompiler.cmake index bc3a62a48c..4d16222cbe 100644 --- a/cmake/RippledCompiler.cmake +++ b/cmake/RippledCompiler.cmake @@ -16,13 +16,16 @@ set(CMAKE_CXX_EXTENSIONS OFF) target_compile_definitions (common INTERFACE $<$:DEBUG _DEBUG> - $<$,$>>:NDEBUG>) - # ^^^^ NOTE: CMAKE release builds already have NDEBUG - # defined, so no need to add it explicitly except for - # this special case of (profile ON) and (assert OFF) - # -- presumably this is because we don't want profile - # builds asserting unless asserts were specifically - # requested + #[===[ + NOTE: CMAKE release builds already have NDEBUG defined, so no need to add it + explicitly except for the special case of (profile ON) and (assert OFF). + Presumably this is because we don't want profile builds asserting unless + asserts were specifically requested. + ]===] + $<$,$>>:NDEBUG> + # TODO: Remove once we have migrated functions from OpenSSL 1.x to 3.x. + OPENSSL_SUPPRESS_DEPRECATED +) if (MSVC) # remove existing exception flag since we set it to -EHa diff --git a/conan.lock b/conan.lock index ec790e16ce..9f52c606a7 100644 --- a/conan.lock +++ b/conan.lock @@ -9,7 +9,7 @@ "rocksdb/10.0.1#85537f46e538974d67da0c3977de48ac%1756234304.347", "re2/20230301#dfd6e2bf050eb90ddd8729cfb4c844a4%1756234257.976", "protobuf/3.21.12#d927114e28de9f4691a6bbcdd9a529d1%1756234251.614", - "openssl/1.1.1w#a8f0792d7c5121b954578a7149d23e03%1756223730.729", + "openssl/3.6.0#89e8af1d4a21afcac0557079d23d8890%1759746682.365", "nudb/2.0.9#c62cfd501e57055a7e0d8ee3d5e5427d%1756234237.107", "lz4/1.10.0#59fc63cac7f10fbe8e05c7e62c2f3504%1756234228.999", "libiconv/1.17#1e65319e945f2d31941a9d28cc13c058%1756223727.64", diff --git a/conanfile.py b/conanfile.py index 3146b887e0..4cd9bd3c5a 100644 --- a/conanfile.py +++ b/conanfile.py @@ -27,7 +27,7 @@ class Xrpl(ConanFile): 'grpc/1.50.1', 'libarchive/3.8.1', 'nudb/2.0.9', - 'openssl/1.1.1w', + 'openssl/3.6.0', 'soci/4.0.3', 'zlib/1.3.1', ]