diff --git a/src/interfaces/node.h b/src/interfaces/node.h index f3362cefba6b..22cda4e8fa51 100644 --- a/src/interfaces/node.h +++ b/src/interfaces/node.h @@ -75,6 +75,11 @@ class MnEntry virtual bool isBanned() const = 0; virtual CService getNetInfoPrimary() const = 0; + //! Platform HTTPS (DAPI gateway) endpoints from the extended address + //! list; empty for non-evo masternodes. Domain-based entries are + //! skipped rather than resolved here — the first entry is always a + //! CService, so an evonode never contributes an empty set. + virtual std::vector getPlatformHTTPSAddrs() const = 0; virtual MnType getType() const = 0; virtual UniValue toJson() const = 0; virtual const CKeyID& getKeyIdOwner() const = 0; @@ -235,6 +240,19 @@ class LLMQ int32_t m_expiry_height{0}; }; virtual std::vector getQuorumStats() = 0; + struct PlatformQuorum { + uint256 m_quorum_hash{}; + std::vector m_pubkey{}; //!< serialized BLS public key (basic scheme) + int32_t m_height{0}; + }; + //! Locally retained quorums of the given LLMQ type with their public + //! keys. Used by the GUI Platform client to verify Platform state-root + //! quorum signatures against locally synced quorum data. This includes + //! retained signing quorums that are no longer in the active signing set. + virtual std::vector getPlatformQuorums(uint8_t llmq_type) = 0; + //! Serialized InstantSend lock for the given txid, or empty if the + //! transaction has no islock (used to build asset lock proofs). + virtual std::vector getInstantSendLock(const uint256& txid) = 0; virtual void setContext(node::NodeContext* context) {} }; diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index c57159887945..14b9fd4a204b 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -146,6 +147,27 @@ class MnEntryImpl : public MnEntry bool isBanned() const override { return m_dmn->pdmnState->IsBanned(); } CService getNetInfoPrimary() const override { return m_dmn->pdmnState->netInfo->GetPrimary(); } + std::vector getPlatformHTTPSAddrs() const override + { + std::vector ret; + if (m_dmn->pdmnState->nVersion < ProTxVersion::ExtAddr) { + // Before ExtAddr the Platform ports are scalar fields paired with + // the primary address instead of netInfo entries, so an evonode + // that has not submitted an extended-address update would + // otherwise contribute no gateway at all. + if (m_dmn->nType == MnType::Evo && m_dmn->pdmnState->platformHTTPPort != 0) { + ret.emplace_back(m_dmn->pdmnState->netInfo->GetPrimary(), + m_dmn->pdmnState->platformHTTPPort); + } + return ret; + } + for (const auto& entry : m_dmn->pdmnState->netInfo->GetEntries(NetInfoPurpose::PLATFORM_HTTPS)) { + if (const auto service_opt{entry.GetAddrPort()}) { + ret.push_back(*service_opt); + } + } + return ret; + } MnType getType() const override { return m_dmn->nType; } UniValue toJson() const override { return m_dmn->ToJson(); } const CKeyID& getKeyIdOwner() const override { return m_dmn->pdmnState->keyIDOwner; } @@ -585,6 +607,58 @@ class LLMQImpl : public LLMQ } return stats; } + std::vector getPlatformQuorums(uint8_t llmq_type) override + { + std::vector ret; + if (!context().llmq_ctx || !context().llmq_ctx->quorum_block_processor || !context().chainman) { + return ret; + } + const auto* pindex{WITH_LOCK(::cs_main, return context().chainman->ActiveChain().Tip())}; + if (!pindex) { + return ret; + } + const auto type{static_cast(llmq_type)}; + const auto llmq_params{Params().GetLLMQ(type)}; + if (!llmq_params.has_value()) { + return ret; + } + // Drive proofs may be signed by an older Platform quorum while they + // are still consensus-valid and retained locally. Export the full + // retained-key window, not only the current signing-active set. + const auto quorum_count{static_cast(std::max(llmq_params->signingActiveQuorumCount, + llmq_params->keepOldKeys))}; + // Read mined final commitments directly: they already carry the quorum + // hash and public key, so there is no need to materialize full CQuorum + // objects (member lists, vvec/contribution reads, quorum cache inserts) + // via ScanQuorums. Newest-first, matching ScanQuorums' ordering. + const auto& qbp{*context().llmq_ctx->quorum_block_processor}; + const auto quorum_base_block_indexes{llmq_params->useRotation + ? qbp.GetMinedCommitmentsIndexedUntilBlock(type, pindex, quorum_count) + : qbp.GetMinedCommitmentsUntilBlock(type, pindex, quorum_count)}; + for (const auto* pQuorumBaseBlockIndex : quorum_base_block_indexes) { + const auto qc{qbp.GetMinedCommitment(type, pQuorumBaseBlockIndex->GetBlockHash()).first}; + if (!qc.quorumPublicKey.IsValid()) continue; + ret.emplace_back(PlatformQuorum{ + .m_quorum_hash = qc.quorumHash, + .m_pubkey = qc.quorumPublicKey.ToByteVector(/*specificLegacyScheme=*/false), + .m_height = pQuorumBaseBlockIndex->nHeight, + }); + } + return ret; + } + std::vector getInstantSendLock(const uint256& txid) override + { + if (!context().llmq_ctx || !context().llmq_ctx->isman) { + return {}; + } + const auto islock{context().llmq_ctx->isman->GetInstantSendLockByTxid(txid)}; + if (!islock) { + return {}; + } + CDataStream ds(SER_NETWORK, PROTOCOL_VERSION); + ds << *islock; + return {UCharCast(ds.data()), UCharCast(ds.data()) + ds.size()}; + } void setContext(NodeContext* context) override { m_context = context; diff --git a/src/test/evo_cbtx_tests.cpp b/src/test/evo_cbtx_tests.cpp index 2e8231812d87..da60ae4a12e9 100644 --- a/src/test/evo_cbtx_tests.cpp +++ b/src/test/evo_cbtx_tests.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #include #include @@ -28,8 +27,6 @@ #include #include -#include -#include #include #include @@ -85,29 +82,6 @@ BOOST_FIXTURE_TEST_CASE(check_cbtx_best_chainlock_rejects_excessive_height_diff, } namespace { -// Mirrors private DB keys in llmq/blockprocessor.cpp so tests can install -// mined-commitment state without a full DKG/mining path. -static const std::string DB_MINED_COMMITMENT = "q_mc"; -static const std::string DB_MINED_COMMITMENT_BY_INVERSED_HEIGHT = "q_mcih"; - -std::tuple BuildInversedHeightKey(Consensus::LLMQType llmqType, int nMinedHeight) -{ - return std::make_tuple(DB_MINED_COMMITMENT_BY_INVERSED_HEIGHT, llmqType, - htobe32_internal(std::numeric_limits::max() - nMinedHeight)); -} - -// Store a mined commitment as if it was mined at `mined_height` for the genesis -// quorum base (quorumHeight 0). GetMinedCommitmentsUntilBlock iterates inverted- -// height keys in [pindex->nHeight, 0), so scan height must be >= mined_height -// and mined_height must be > 0 for the entry to be returned. -void WriteMinedCommitment(CEvoDB& evoDb, const CFinalCommitment& qc, const uint256& mined_block_hash, int mined_height) -{ - assert(mined_height > 0); - evoDb.Write(std::make_pair(DB_MINED_COMMITMENT, std::make_pair(qc.llmqType, qc.quorumHash)), - std::make_pair(qc, mined_block_hash)); - evoDb.Write(BuildInversedHeightKey(qc.llmqType, mined_height), /*quorumHeight=*/0); -} - CTransactionRef MakeCommitmentTx(const CFinalCommitment& qc, int height) { CFinalCommitmentTxPayload payload; @@ -191,7 +165,7 @@ BOOST_FIXTURE_TEST_CASE(qc_hash_cache_invalidated_by_undoblock, Dip3ActiveSetup) { auto dbTx = evoDb.BeginTransaction(); - WriteMinedCommitment(evoDb, qc_a, mined_hash_a, mined_height); + WriteMinedCommitment(evoDb, qc_a, mined_hash_a, mined_height, /*quorum_height=*/0); dbTx->Commit(); } @@ -211,7 +185,7 @@ BOOST_FIXTURE_TEST_CASE(qc_hash_cache_invalidated_by_undoblock, Dip3ActiveSetup) auto dbTx = evoDb.BeginTransaction(); BOOST_REQUIRE(qblockman.UndoBlock(m_node.chainman->ActiveChainstate(), block_with_qc, &pindex_mined)); // Install the replacement while the disconnect transaction is still open. - WriteMinedCommitment(evoDb, qc_b, mined_hash_b, mined_height); + WriteMinedCommitment(evoDb, qc_b, mined_hash_b, mined_height, /*quorum_height=*/0); dbTx->Commit(); } diff --git a/src/test/evo_deterministicmns_tests.cpp b/src/test/evo_deterministicmns_tests.cpp index 8ca40a2d2909..2e4d117ab182 100644 --- a/src/test/evo_deterministicmns_tests.cpp +++ b/src/test/evo_deterministicmns_tests.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -1895,6 +1896,159 @@ BOOST_AUTO_TEST_CASE(proupserv_migrates_legacy) FuncProUpServTxMigratesLegacy(setup); } +// The Platform GUI consumes DAPI gateway endpoints through the node interface +// (MnEntry::getPlatformHTTPSAddrs). Both EvoNode generations must export a gateway: a pre-ExtAddr +// registration carries the Platform HTTPS port as a scalar paired with the primary core address, +// while an ExtAddr registration carries explicit PLATFORM_HTTPS netInfo entries where domain +// entries are skipped (unresolvable without DNS lookups the interface must not perform) and the +// leading CService entry keeps the EvoNode reachable. Regular masternodes export nothing. +void FuncPlatformHTTPSAddrsViaNodeInterface(TestChainV24SignalBeforeV19Setup& setup) +{ + auto& chainman = setup.chainman; + auto& dmnman = setup.dmnman; + auto& utxos = setup.utxos; + + setup.MineToV19(); + BOOST_REQUIRE(!setup.IsV24Active()); + BOOST_REQUIRE(!bls::bls_legacy_scheme.load()); + + // Regular masternode: no Platform endpoints to export. + CKey regular_owner_key; + CBLSSecretKey regular_operator_key; + auto tx_reg_regular = CreateProRegTx(chainman, utxos, 20000, GenerateRandomAddress(), setup.coinbaseKey, + regular_owner_key, regular_operator_key); + const auto regular_protx_hash = tx_reg_regular.GetHash(); + setup.ProcessBlock({tx_reg_regular}); + BOOST_REQUIRE(dmnman.GetListAtChainTip().HasMN(regular_protx_hash)); + + // Legacy (pre-ExtAddr) EvoNode: the Platform HTTPS port is a scalar field, the gateway address + // is the primary core address paired with it. + CKey evo_owner_key; + evo_owner_key.MakeNewKey(true); + CBLSSecretKey evo_operator_key; + evo_operator_key.MakeNewKey(); + CProRegTx pro_reg; + pro_reg.nVersion = ProTxVersion::BasicBLS; + pro_reg.nType = MnType::Evo; + pro_reg.netInfo = NetInfoInterface::MakeNetInfo(pro_reg.nVersion); + pro_reg.collateralOutpoint.n = 0; + BOOST_REQUIRE_EQUAL(pro_reg.netInfo->AddEntry(NetInfoPurpose::CORE_P2P, "1.1.1.2:20100"), NetInfoStatus::Success); + pro_reg.platformNodeID.SetHex("00112233445566778899aabbccddeeff00112233"); + pro_reg.platformP2PPort = 20101; + pro_reg.platformHTTPPort = 20102; + pro_reg.keyIDOwner = evo_owner_key.GetPubKey().GetID(); + pro_reg.pubKeyOperator.Set(evo_operator_key.GetPublicKey(), bls::bls_legacy_scheme.load()); + pro_reg.keyIDVoting = evo_owner_key.GetPubKey().GetID(); + pro_reg.scriptPayout = GenerateRandomAddress(); + CMutableTransaction tx_reg_evo; + tx_reg_evo.nVersion = 3; + tx_reg_evo.nType = TRANSACTION_PROVIDER_REGISTER; + { + const auto spent = FundTransaction(chainman, tx_reg_evo, utxos, pro_reg.scriptPayout, + dmn_types::Evo.collat_amount); + pro_reg.inputsHash = CalcTxInputsHash(CTransaction(tx_reg_evo)); + SetTxPayload(tx_reg_evo, pro_reg); + SignTransaction(tx_reg_evo, spent, setup.coinbaseKey); + } + const auto evo_protx_hash = tx_reg_evo.GetHash(); + setup.ProcessBlock({tx_reg_evo}); + BOOST_REQUIRE(dmnman.GetListAtChainTip().HasMN(evo_protx_hash)); + + setup.MineToV24(); + // Not upgraded to ExtAddr by activation alone: the state keeps the scalar-port layout. + BOOST_REQUIRE_EQUAL(dmnman.GetListAtChainTip().GetMN(evo_protx_hash)->pdmnState->nVersion, + ProTxVersion::BasicBLS); + + // ExtAddr EvoNode: explicit PLATFORM_HTTPS entries, a CService first and a domain second. + CKey evo2_owner_key; + evo2_owner_key.MakeNewKey(true); + CBLSSecretKey evo2_operator_key; + evo2_operator_key.MakeNewKey(); + CProRegTx pro_reg2; + pro_reg2.nVersion = ProTxVersion::ExtAddr; + pro_reg2.nType = MnType::Evo; + pro_reg2.netInfo = NetInfoInterface::MakeNetInfo(pro_reg2.nVersion); + pro_reg2.collateralOutpoint.n = 0; + BOOST_REQUIRE_EQUAL(pro_reg2.netInfo->AddEntry(NetInfoPurpose::CORE_P2P, "1.1.1.3:20200"), NetInfoStatus::Success); + BOOST_REQUIRE_EQUAL(pro_reg2.netInfo->AddEntry(NetInfoPurpose::PLATFORM_P2P, "1.1.1.3:20201"), + NetInfoStatus::Success); + BOOST_REQUIRE_EQUAL(pro_reg2.netInfo->AddEntry(NetInfoPurpose::PLATFORM_HTTPS, "1.1.1.3:20202"), + NetInfoStatus::Success); + BOOST_REQUIRE_EQUAL(pro_reg2.netInfo->AddEntry(NetInfoPurpose::PLATFORM_HTTPS, "platform.example.com:443"), + NetInfoStatus::Success); + pro_reg2.platformNodeID.SetHex("445566778899aabbccddeeff0011223344556677"); + pro_reg2.keyIDOwner = evo2_owner_key.GetPubKey().GetID(); + pro_reg2.pubKeyOperator.Set(evo2_operator_key.GetPublicKey(), /*specificLegacyScheme=*/false); + pro_reg2.keyIDVoting = evo2_owner_key.GetPubKey().GetID(); + pro_reg2.payouts = {{GenerateRandomAddress(), MasternodePayoutShare::MAX_REWARD}}; + CMutableTransaction tx_reg_evo2; + tx_reg_evo2.nVersion = 3; + tx_reg_evo2.nType = TRANSACTION_PROVIDER_REGISTER; + { + // The collateral must not pay to a payout script from ExtAddr onwards, so fund it from and + // change back to the coinbase key's script. + const auto spent = FundTransaction(chainman, tx_reg_evo2, utxos, + GetScriptForDestination(PKHash(setup.coinbaseKey.GetPubKey())), + dmn_types::Evo.collat_amount); + pro_reg2.inputsHash = CalcTxInputsHash(CTransaction(tx_reg_evo2)); + SetTxPayload(tx_reg_evo2, pro_reg2); + SignTransaction(tx_reg_evo2, spent, setup.coinbaseKey); + } + const auto evo2_protx_hash = tx_reg_evo2.GetHash(); + setup.ProcessBlock({tx_reg_evo2}); + BOOST_REQUIRE(dmnman.GetListAtChainTip().HasMN(evo2_protx_hash)); + + auto node{interfaces::MakeNode(setup.m_node)}; + auto [mn_list, tip] = node->evo().getListAtChainTip(); + BOOST_REQUIRE(mn_list != nullptr); + BOOST_REQUIRE(tip != nullptr); + + std::map entries; + mn_list->forEachMN(/*only_valid=*/false, + [&entries](const interfaces::MnEntryCPtr& mn) { entries.emplace(mn->getProTxHash(), mn); }); + BOOST_REQUIRE_EQUAL(entries.size(), 3U); + + // Regular masternode contributes nothing. + BOOST_CHECK(entries.at(regular_protx_hash)->getType() == MnType::Regular); + BOOST_CHECK(entries.at(regular_protx_hash)->getPlatformHTTPSAddrs().empty()); + + // Legacy EvoNode: primary core address paired with the scalar HTTPS port. + { + const auto& entry = entries.at(evo_protx_hash); + BOOST_CHECK(entry->getType() == MnType::Evo); + const auto addrs = entry->getPlatformHTTPSAddrs(); + BOOST_REQUIRE_EQUAL(addrs.size(), 1U); + BOOST_CHECK(addrs[0] == LookupNumeric("1.1.1.2", 20102)); + BOOST_CHECK(static_cast(addrs[0]) == static_cast(entry->getNetInfoPrimary())); + } + + // ExtAddr EvoNode: the CService entry is exported, the domain entry is skipped. + { + const auto& entry = entries.at(evo2_protx_hash); + BOOST_CHECK(entry->getType() == MnType::Evo); + const auto addrs = entry->getPlatformHTTPSAddrs(); + BOOST_REQUIRE_EQUAL(addrs.size(), 1U); + BOOST_CHECK(addrs[0] == LookupNumeric("1.1.1.3", 20202)); + + // Pin the shape the interface relies on: the first stored PLATFORM_HTTPS entry is a + // CService, so an EvoNode never contributes an empty gateway set even though domain + // entries are skipped. + const auto dmn = dmnman.GetListAtChainTip().GetMN(evo2_protx_hash); + BOOST_REQUIRE(dmn != nullptr); + const auto https_entries = dmn->pdmnState->netInfo->GetEntries(NetInfoPurpose::PLATFORM_HTTPS); + BOOST_REQUIRE_EQUAL(https_entries.size(), 2U); + BOOST_REQUIRE(https_entries[0].GetAddrPort().has_value()); + BOOST_CHECK(addrs[0] == *https_entries[0].GetAddrPort()); + BOOST_CHECK(https_entries[1].GetDomainPort().has_value()); + } +} + +BOOST_AUTO_TEST_CASE(platform_https_addrs_via_node_interface) +{ + TestChainV24SignalBeforeV19Setup setup; + FuncPlatformHTTPSAddrsViaNodeInterface(setup); +} + // The SAME masternode, two registrar updates in one block, version-crossing. tx1 rotates a // legacy MN to a new key at v2 (making it BasicBLS); tx2 then rotates it to another new key at v1. // tx2 passes CheckProUpRegTx against pindexPrev (the MN was legacy there), but in the rebuild the MN diff --git a/src/test/interfaces_tests.cpp b/src/test/interfaces_tests.cpp index 6dfd816b0721..d96112dbca1e 100644 --- a/src/test/interfaces_tests.cpp +++ b/src/test/interfaces_tests.cpp @@ -2,14 +2,29 @@ // 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