From acb01a2c4566daa450d8ac5a87e78ab02133c6fa Mon Sep 17 00:00:00 2001 From: pasta Date: Sat, 15 Aug 2026 12:51:55 -0500 Subject: [PATCH 1/4] refactor(evo): extract shared provider network-field validation --- src/evo/providertx.cpp | 97 +++++++++++++++++++++++++++++++++++----- src/evo/providertx.h | 6 +++ src/evo/specialtxman.cpp | 89 ++---------------------------------- 3 files changed, 96 insertions(+), 96 deletions(-) diff --git a/src/evo/providertx.cpp b/src/evo/providertx.cpp index 9ccf89abfc61..ef133a9a5cb5 100644 --- a/src/evo/providertx.cpp +++ b/src/evo/providertx.cpp @@ -79,30 +79,105 @@ bool IsPayoutListKeySafe(const MasternodePayoutShares& payouts, const CTxDestina return true; } -template -bool IsNetInfoTriviallyValid(const ProTx& proTx, TxValidationState& state) +static bool IsNetInfoTriviallyValid(const std::shared_ptr& net_info, MnType type, TxValidationState& state) { - if (!proTx.netInfo->HasEntries(NetInfoPurpose::CORE_P2P)) { + if (!net_info->HasEntries(NetInfoPurpose::CORE_P2P)) { // Mandatory for all nodes return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-empty"); } - if (proTx.nType == MnType::Regular) { + if (type == MnType::Regular) { // Regular nodes shouldn't populate Platform-specific fields - if (proTx.netInfo->HasEntries(NetInfoPurpose::PLATFORM_HTTPS) || - proTx.netInfo->HasEntries(NetInfoPurpose::PLATFORM_P2P)) { + if (net_info->HasEntries(NetInfoPurpose::PLATFORM_HTTPS) || net_info->HasEntries(NetInfoPurpose::PLATFORM_P2P)) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-bad"); } } - if (proTx.netInfo->CanStorePlatform() && proTx.nType == MnType::Evo) { + if (net_info->CanStorePlatform() && type == MnType::Evo) { // Platform fields are mandatory for EvoNodes - if (!proTx.netInfo->HasEntries(NetInfoPurpose::PLATFORM_HTTPS) || - !proTx.netInfo->HasEntries(NetInfoPurpose::PLATFORM_P2P)) { + if (!net_info->HasEntries(NetInfoPurpose::PLATFORM_HTTPS) || !net_info->HasEntries(NetInfoPurpose::PLATFORM_P2P)) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-empty"); } } return true; } +static bool CheckNetInfo(const NetInfoInterface& net_info, TxValidationState& state) +{ + switch (net_info.Validate()) { + case NetInfoStatus::BadAddress: + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-addr"); + case NetInfoStatus::BadPort: + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-port"); + case NetInfoStatus::BadType: + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-addr-type"); + case NetInfoStatus::NotRoutable: + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-addr-unroutable"); + case NetInfoStatus::Malformed: + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-bad"); + case NetInfoStatus::Success: + return true; + case NetInfoStatus::BadInput: + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-entry"); + case NetInfoStatus::Duplicate: + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-dup-netinfo-entry"); + case NetInfoStatus::MaxLimit: + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-maxlimit"); + } + assert(false); +} + +bool CheckProviderNetworkFields(const std::shared_ptr& net_info, MnType type, uint16_t version, + const uint160* platform_node_id, uint16_t platform_p2p_port, + uint16_t platform_http_port, bool allow_empty, TxValidationState& state) +{ + if (!net_info || net_info->CanStorePlatform() != (version >= ProTxVersion::ExtAddr)) { + return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-protx-netinfo-version"); + } + if (net_info->IsEmpty()) { + if (!allow_empty) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-empty"); + } + } else { + if (!IsNetInfoTriviallyValid(net_info, type, state) || !CheckNetInfo(*net_info, state)) { + return false; + } + } + + if (type != MnType::Evo) return true; + if (platform_node_id && platform_node_id->IsNull()) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-platform-nodeid"); + } + if (version >= ProTxVersion::ExtAddr) { + if (platform_p2p_port != 0) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-platform-p2p-port"); + } + if (platform_http_port != 0) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-platform-http-port"); + } + return true; + } + + if (::IsNodeOnMainnet()) { + if (platform_p2p_port != ::MainParams().GetDefaultPlatformP2PPort()) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-platform-p2p-port"); + } + if (platform_http_port != ::MainParams().GetDefaultPlatformHTTPPort()) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-platform-http-port"); + } + } + if (platform_p2p_port == ::MainParams().GetDefaultPort()) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-platform-p2p-port"); + } + if (platform_http_port == ::MainParams().GetDefaultPort()) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-platform-http-port"); + } + + const uint16_t core_port{net_info->GetPrimary().GetPort()}; + if (platform_p2p_port == platform_http_port || platform_p2p_port == core_port || platform_http_port == core_port) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-platform-dup-ports"); + } + return true; +} + bool CProRegTx::IsTriviallyValid(TxValidationState& state) const { if (nVersion == 0 || nVersion > ProTxVersion::ExtAddr) { @@ -129,7 +204,7 @@ bool CProRegTx::IsTriviallyValid(TxValidationState& state) const if (netInfo->CanStorePlatform() != (nVersion >= ProTxVersion::ExtAddr)) { return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-protx-netinfo-version"); } - if (!netInfo->IsEmpty() && !IsNetInfoTriviallyValid(*this, state)) { + if (!netInfo->IsEmpty() && !IsNetInfoTriviallyValid(netInfo, nType, state)) { // pass the state returned by the function above return false; } @@ -199,7 +274,7 @@ bool CProUpServTx::IsTriviallyValid(TxValidationState& state) const if (netInfo->IsEmpty()) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-empty"); } - if (!IsNetInfoTriviallyValid(*this, state)) { + if (!IsNetInfoTriviallyValid(netInfo, nType, state)) { // pass the state returned by the function above return false; } diff --git a/src/evo/providertx.h b/src/evo/providertx.h index d27713e74ca7..f38c3bacc059 100644 --- a/src/evo/providertx.h +++ b/src/evo/providertx.h @@ -58,6 +58,12 @@ template [[nodiscard]] std::string PayoutListToString(const MasternodePayoutShares& payouts); [[nodiscard]] UniValue PayoutListToJson(const MasternodePayoutShares& payouts); +/** Validate all provider network fields using the same rules as special transaction validation. + * Pass nullptr for platform_node_id when validating endpoint input separately from the rest of a payload. */ +[[nodiscard]] bool CheckProviderNetworkFields(const std::shared_ptr& net_info, MnType type, + uint16_t version, const uint160* platform_node_id, uint16_t platform_p2p_port, + uint16_t platform_http_port, bool allow_empty, TxValidationState& state); + class CProRegTx { public: diff --git a/src/evo/specialtxman.cpp b/src/evo/specialtxman.cpp index 73c08df28492..bf3f11ba5df3 100644 --- a/src/evo/specialtxman.cpp +++ b/src/evo/specialtxman.cpp @@ -961,75 +961,6 @@ bool CSpecialTxProcessor::CheckCreditPoolDiffForBlock(const CBlock& block, const return true; } -template -static bool CheckService(const ProTx& proTx, TxValidationState& state) -{ - switch (proTx.netInfo->Validate()) { - case NetInfoStatus::BadAddress: - return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-addr"); - case NetInfoStatus::BadPort: - return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-port"); - case NetInfoStatus::BadType: - return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-addr-type"); - case NetInfoStatus::NotRoutable: - return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-addr-unroutable"); - case NetInfoStatus::Malformed: - return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-bad"); - case NetInfoStatus::Success: - return true; - // Reachable through a serialized netInfo that bypasses the in-memory builder's checks - case NetInfoStatus::BadInput: - return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-entry"); - case NetInfoStatus::Duplicate: - return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-dup-netinfo-entry"); - case NetInfoStatus::MaxLimit: - return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-maxlimit"); - } // no default case, so the compiler can warn about missing cases - assert(false); -} - -template -static bool CheckPlatformFields(const ProTx& proTx, bool is_extended_addr, TxValidationState& state) -{ - if (proTx.platformNodeID.IsNull()) { - return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-platform-nodeid"); - } - - if (is_extended_addr) { - // platformHTTPPort and platformP2PPort have been subsumed by netInfo. They should always be zero. - if (proTx.platformP2PPort != 0) { - return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-platform-p2p-port"); - } - if (proTx.platformHTTPPort != 0) { - return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-platform-http-port"); - } - return true; - } - - if (::IsNodeOnMainnet()) { - if (proTx.platformP2PPort != ::MainParams().GetDefaultPlatformP2PPort()) { - return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-platform-p2p-port"); - } - if (proTx.platformHTTPPort != ::MainParams().GetDefaultPlatformHTTPPort()) { - return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-platform-http-port"); - } - } - if (proTx.platformP2PPort == ::MainParams().GetDefaultPort()) { - return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-platform-p2p-port"); - } - if (proTx.platformHTTPPort == ::MainParams().GetDefaultPort()) { - return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-platform-http-port"); - } - - const uint16_t core_port{proTx.netInfo->GetPrimary().GetPort()}; - if (proTx.platformP2PPort == proTx.platformHTTPPort || proTx.platformP2PPort == core_port || - proTx.platformHTTPPort == core_port) { - return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-platform-dup-ports"); - } - - return true; -} - template static bool CheckHashSig(const ProTx& proTx, const PKHash& pkhash, TxValidationState& state) { @@ -1126,17 +1057,11 @@ bool CheckProRegTx(const CTransaction& tx, gsl::not_null pin // It's allowed to set addr to 0, which will put the MN into PoSe-banned state and require a ProUpServTx to be // issues later. If any of both is set, it must be valid however - if (!opt_ptx->netInfo->IsEmpty() && !CheckService(*opt_ptx, state)) { - // pass the state returned by the function above + if (!CheckProviderNetworkFields(opt_ptx->netInfo, opt_ptx->nType, opt_ptx->nVersion, &opt_ptx->platformNodeID, + opt_ptx->platformP2PPort, opt_ptx->platformHTTPPort, /*allow_empty=*/true, state)) { return false; } - if (opt_ptx->nType == MnType::Evo) { - if (!CheckPlatformFields(*opt_ptx, opt_ptx->nVersion >= ProTxVersion::ExtAddr, state)) { - return false; - } - } - CTxDestination collateralTxDest; const PKHash* keyForPayloadSig = nullptr; COutPoint collateralOutpoint; @@ -1260,17 +1185,11 @@ bool CheckProUpServTx(const CTransaction& tx, gsl::not_null return false; } - if (!CheckService(*opt_ptx, state)) { - // pass the state returned by the function above + if (!CheckProviderNetworkFields(opt_ptx->netInfo, opt_ptx->nType, opt_ptx->nVersion, &opt_ptx->platformNodeID, + opt_ptx->platformP2PPort, opt_ptx->platformHTTPPort, /*allow_empty=*/false, state)) { return false; } - if (opt_ptx->nType == MnType::Evo) { - if (!CheckPlatformFields(*opt_ptx, opt_ptx->nVersion >= ProTxVersion::ExtAddr, state)) { - return false; - } - } - auto mnList = dmnman.GetListForBlock(pindexPrev); auto dmn = mnList.GetMN(opt_ptx->proTxHash); if (!dmn) { From bde93a47a5f9e947d6c9f16a9f7f3c40aedeed9a Mon Sep 17 00:00:00 2001 From: pasta Date: Sat, 15 Aug 2026 12:52:02 -0500 Subject: [PATCH 2/4] feat(interfaces): add wallet fund, sign and coin-lock primitives --- src/interfaces/wallet.h | 27 ++++++++++ src/wallet/interfaces.cpp | 88 ++++++++++++++++++++++++++++++++ src/wallet/test/wallet_tests.cpp | 88 ++++++++++++++++++++++++++++++++ 3 files changed, 203 insertions(+) diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h index 7300f4286e99..9fc779906135 100644 --- a/src/interfaces/wallet.h +++ b/src/interfaces/wallet.h @@ -69,6 +69,18 @@ class Loader; using WalletOrderForm = std::vector>; using WalletValueMap = std::map; +struct WalletTxSignResult { + CTransactionRef tx; + bool complete{false}; + std::vector errors; +}; + +enum class CoinLockResult { + ACQUIRED, + ALREADY_LOCKED, + FAILED, +}; + //! Interface for accessing a wallet. class Wallet { @@ -164,6 +176,9 @@ class Wallet //! Lock coin. virtual bool lockCoin(const COutPoint& output, const bool write_to_db) = 0; + //! Atomically lock a coin only when it is not already locked. + virtual CoinLockResult acquireCoinLock(const COutPoint& output, bool write_to_db) = 0; + //! Unlock coin. virtual bool unlockCoin(const COutPoint& output) = 0; @@ -195,6 +210,18 @@ class Wallet int& change_pos, CAmount& fee) = 0; + //! Fund a transaction template per the provider-transaction funding policy: + //! inputs are selected only from coins received at the single specified + //! fee-source destination, change is returned to that same destination, and + //! a temporary dummy output is added (and later removed) when the template + //! has no outputs. Not a generic funding primitive. The template's version, + //! type, payload, and required outputs are preserved. + virtual util::Result fundTransaction(const CMutableTransaction& tx_template, + const CTxDestination& fund_destination) = 0; + + //! Sign every wallet-owned input of a transaction and report whether signing is complete. + virtual util::Result signTransaction(const CMutableTransaction& tx) = 0; + //! Commit transaction. virtual void commitTransaction(CTransactionRef tx, WalletValueMap value_map, diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp index 099437d099a3..4af755044de8 100644 --- a/src/wallet/interfaces.cpp +++ b/src/wallet/interfaces.cpp @@ -45,6 +45,7 @@ #include using interfaces::Chain; +using interfaces::CoinLockResult; using interfaces::FoundBlock; using interfaces::Handler; using interfaces::MakeHandler; @@ -55,6 +56,7 @@ using interfaces::WalletLoader; using interfaces::WalletOrderForm; using interfaces::WalletTx; using interfaces::WalletTxOut; +using interfaces::WalletTxSignResult; using interfaces::WalletTxStatus; using interfaces::WalletValueMap; using node::NodeContext; @@ -325,6 +327,20 @@ class WalletImpl : public Wallet std::unique_ptr batch = write_to_db ? std::make_unique(m_wallet->GetDatabase()) : nullptr; return m_wallet->LockCoin(output, batch.get()); } + CoinLockResult acquireCoinLock(const COutPoint& output, bool write_to_db) override + { + LOCK(m_wallet->cs_wallet); + if (m_wallet->IsLockedCoin(output)) return CoinLockResult::ALREADY_LOCKED; + std::unique_ptr batch = write_to_db ? std::make_unique(m_wallet->GetDatabase()) : nullptr; + if (!m_wallet->LockCoin(output, batch.get())) { + // LockCoin inserts into memory before persisting; the coin was not + // locked on entry, so undo the insertion (and erase any partially + // persisted record) to keep FAILED meaning "no lock acquired". + m_wallet->UnlockCoin(output, batch.get()); + return CoinLockResult::FAILED; + } + return CoinLockResult::ACQUIRED; + } bool unlockCoin(const COutPoint& output) override { LOCK(m_wallet->cs_wallet); @@ -389,6 +405,78 @@ class WalletImpl : public Wallet return txr.tx; } + util::Result fundTransaction(const CMutableTransaction& tx_template, + const CTxDestination& fund_destination) override + { + // Callers must not hold cs_main: the wallet library cannot reference + // node locks, and syncing below would deadlock if it were held. + m_wallet->BlockUntilSyncedToCurrentChain(); + + LOCK(m_wallet->cs_wallet); + if (std::holds_alternative(fund_destination)) { + return util::Error{Untranslated("No source of funds specified")}; + } + + CMutableTransaction tx{tx_template}; + static const CTxOut dummy_output{0, CScript() << OP_RETURN}; + const bool add_dummy_output{tx.vout.empty()}; + if (add_dummy_output) tx.vout.emplace_back(dummy_output); + + std::vector recipients; + recipients.reserve(tx.vout.size()); + for (const auto& output : tx.vout) { + recipients.push_back({output.scriptPubKey, output.nValue, /*fSubtractFeeFromAmount=*/false}); + } + + CCoinControl coin_control; + coin_control.destChange = fund_destination; + coin_control.fRequireAllInputs = false; + for (const auto& output : AvailableCoinsListUnspent(*m_wallet).all()) { + CTxDestination destination; + if (ExtractDestination(output.txout.scriptPubKey, destination) && destination == fund_destination) { + coin_control.Select(output.outpoint); + } + } + if (!coin_control.HasSelected()) { + return util::Error{ + strprintf(Untranslated("No funds at specified address %s"), EncodeDestination(fund_destination))}; + } + + auto result{CreateTransaction(*m_wallet, recipients, RANDOM_CHANGE_POSITION, coin_control, + /*sign=*/true, tx.vExtraPayload.size())}; + if (!result) return util::Error{util::ErrorString(result)}; + + tx.vin = result->tx->vin; + tx.vout = result->tx->vout; + if (add_dummy_output && tx.vout.size() > 1) { + const auto it{std::find(tx.vout.begin(), tx.vout.end(), dummy_output)}; + CHECK_NONFATAL(it != tx.vout.end()); + tx.vout.erase(it); + } + return MakeTransactionRef(std::move(tx)); + } + util::Result signTransaction(const CMutableTransaction& tx) override + { + // Callers must not hold cs_main (see fundTransaction above). + std::map coins; + for (const auto& input : tx.vin) + coins.try_emplace(input.prevout); + m_wallet->chain().findCoins(coins); + + LOCK(m_wallet->cs_wallet); + if (m_wallet->IsLocked()) { + return util::Error{_("Please enter the wallet passphrase with walletpassphrase first.")}; + } + + CMutableTransaction signed_tx{tx}; + std::map input_errors; + const bool complete{m_wallet->SignTransaction(signed_tx, coins, SIGHASH_ALL, input_errors)}; + std::vector errors; + errors.reserve(input_errors.size()); + for (auto& [index, error] : input_errors) + errors.push_back(std::move(error)); + return WalletTxSignResult{MakeTransactionRef(std::move(signed_tx)), complete, std::move(errors)}; + } void commitTransaction(CTransactionRef tx, WalletValueMap value_map, WalletOrderForm order_form) override diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp index ba7fc6640ec6..bd4ad61a182d 100644 --- a/src/wallet/test/wallet_tests.cpp +++ b/src/wallet/test/wallet_tests.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include @@ -49,8 +50,95 @@ extern RPCHelpMan addmultisigaddress(); // as the default levels for node policy. static_assert(DEFAULT_TRANSACTION_MINFEE >= DEFAULT_MIN_RELAY_TX_FEE, "wallet minimum fee is smaller than default relay fee"); +namespace { +//! Database whose writes can be toggled to fail, to prove that a failed +//! persistent coin-lock acquisition leaves the caller owning no lock. +class ToggleFailBatch final : public DatabaseBatch +{ +private: + bool& m_write_success; + bool ReadKey(CDataStream&&, CDataStream&) override { return false; } + bool WriteKey(CDataStream&&, CDataStream&&, bool) override { return m_write_success; } + bool EraseKey(CDataStream&&) override { return m_write_success; } + bool HasKey(CDataStream&&) override { return false; } + bool ErasePrefix(Span) override { return m_write_success; } + +public: + explicit ToggleFailBatch(bool& write_success) : m_write_success(write_success) {} + void Flush() override {} + void Close() override {} + bool StartCursor() override { return true; } + bool ReadAtCursor(CDataStream&, CDataStream&, bool& complete) override + { + complete = true; + return true; + } + void CloseCursor() override {} + bool TxnBegin() override { return true; } + bool TxnCommit() override { return true; } + bool TxnAbort() override { return true; } +}; + +class ToggleFailDatabase final : public WalletDatabase +{ +public: + bool write_success{true}; + + void Open() override {} + void AddRef() override {} + void RemoveRef() override {} + bool Rewrite(const char*) override { return true; } + bool Backup(const std::string&) const override { return true; } + void Flush() override {} + void Close() override {} + bool PeriodicFlush() override { return true; } + void IncrementUpdateCounter() override { ++nUpdateCounter; } + void ReloadDbEnv() override {} + std::string Filename() override { return "toggle-fail"; } + std::string Format() override { return "toggle-fail"; } + std::unique_ptr MakeBatch(bool) override { return std::make_unique(write_success); } +}; +} // namespace + BOOST_FIXTURE_TEST_SUITE(wallet_tests, WalletTestingSetup) +BOOST_AUTO_TEST_CASE(interface_coin_lock_ownership) +{ + const auto wallet_ref{std::shared_ptr(&m_wallet, [](CWallet*) {})}; + auto wallet_interface{interfaces::MakeWallet(*m_wallet_loader->context(), wallet_ref)}; + const COutPoint outpoint{uint256::ONE, 0}; + + BOOST_CHECK(wallet_interface->acquireCoinLock(outpoint, /*write_to_db=*/false) == interfaces::CoinLockResult::ACQUIRED); + BOOST_CHECK(wallet_interface->acquireCoinLock(outpoint, /*write_to_db=*/false) == + interfaces::CoinLockResult::ALREADY_LOCKED); + BOOST_CHECK(wallet_interface->unlockCoin(outpoint)); + BOOST_CHECK(wallet_interface->acquireCoinLock(outpoint, /*write_to_db=*/false) == interfaces::CoinLockResult::ACQUIRED); + BOOST_CHECK(wallet_interface->unlockCoin(outpoint)); +} + +BOOST_AUTO_TEST_CASE(interface_coin_lock_failed_persist) +{ + auto database{std::make_unique()}; + auto* database_ptr{database.get()}; + auto wallet{std::make_shared(m_node.chain.get(), m_coinjoin_loader.get(), "", m_args, + std::move(database))}; + BOOST_REQUIRE(wallet->LoadWallet() == DBErrors::LOAD_OK); + auto wallet_interface{interfaces::MakeWallet(*m_wallet_loader->context(), wallet)}; + const COutPoint outpoint{uint256::ONE, 0}; + + // FAILED must mean no lock was acquired: the in-memory insertion is rolled + // back when the persistent write fails. + database_ptr->write_success = false; + BOOST_CHECK(wallet_interface->acquireCoinLock(outpoint, /*write_to_db=*/true) == + interfaces::CoinLockResult::FAILED); + BOOST_CHECK(!wallet_interface->isLockedCoin(outpoint)); + + database_ptr->write_success = true; + BOOST_CHECK(wallet_interface->acquireCoinLock(outpoint, /*write_to_db=*/true) == + interfaces::CoinLockResult::ACQUIRED); + BOOST_CHECK(wallet_interface->isLockedCoin(outpoint)); +} + static std::shared_ptr TestLoadWallet(WalletContext& context) { DatabaseOptions options; From f2e0ed50527d14fd5de3edf597558478567aec4f Mon Sep 17 00:00:00 2001 From: pasta Date: Sat, 15 Aug 2026 12:52:21 -0500 Subject: [PATCH 3/4] refactor(evo): expose typed provider transaction operations --- doc/release-notes-7600.md | 9 + src/Makefile.am | 3 + src/evo/providertx_service.cpp | 812 ++++++++++++++++++++++++ src/evo/providertx_service.h | 49 ++ src/interfaces/node.h | 22 + src/interfaces/providertx.h | 142 +++++ src/node/interfaces.cpp | 50 +- src/rpc/evo.cpp | 941 ++++++++++------------------ src/rpc/evo_util.cpp | 133 ---- src/rpc/evo_util.h | 8 - src/test/evo_netinfo_tests.cpp | 43 ++ src/test/interfaces_tests.cpp | 103 +++ test/functional/wallet_dash_rpcs.py | 48 +- test/util/data/non-backported.txt | 1 + 14 files changed, 1601 insertions(+), 763 deletions(-) create mode 100644 doc/release-notes-7600.md create mode 100644 src/evo/providertx_service.cpp create mode 100644 src/evo/providertx_service.h create mode 100644 src/interfaces/providertx.h diff --git a/doc/release-notes-7600.md b/doc/release-notes-7600.md new file mode 100644 index 000000000000..626e0cf3801f --- /dev/null +++ b/doc/release-notes-7600.md @@ -0,0 +1,9 @@ +RPC changes +----------- + +- Normal and Evo `protx` registration and maintenance commands now share a + typed provider-transaction implementation with other wallet frontends. RPC + names and successful result formats are unchanged. + `protx update_service` on a masternode whose state does not yield a usable + default fee source now returns an explicit "specify feeSourceAddress" + parameter error instead of an internal error. (#7600) diff --git a/src/Makefile.am b/src/Makefile.am index fb1ffde2529a..0cf8ce802fdb 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -232,6 +232,7 @@ BITCOIN_CORE_H = \ evo/mnhftx.h \ evo/netinfo.h \ evo/providertx.h \ + evo/providertx_service.h \ evo/simplifiedmns.h \ evo/smldiff.h \ evo/specialtx.h \ @@ -281,6 +282,7 @@ BITCOIN_CORE_H = \ interfaces/init.h \ interfaces/ipc.h \ interfaces/node.h \ + interfaces/providertx.h \ interfaces/wallet.h \ kernel/blockmanager_opts.h \ kernel/chain.h \ @@ -539,6 +541,7 @@ libbitcoin_node_a_SOURCES = \ evo/mnauth.cpp \ evo/mnhftx.cpp \ evo/providertx.cpp \ + evo/providertx_service.cpp \ evo/simplifiedmns.cpp \ evo/smldiff.cpp \ evo/specialtx.cpp \ diff --git a/src/evo/providertx_service.cpp b/src/evo/providertx_service.cpp new file mode 100644 index 000000000000..ac1f3428e1fe --- /dev/null +++ b/src/evo/providertx_service.cpp @@ -0,0 +1,812 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include