Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/interfaces/node.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<CService> getPlatformHTTPSAddrs() const = 0;
virtual MnType getType() const = 0;
virtual UniValue toJson() const = 0;
virtual const CKeyID& getKeyIdOwner() const = 0;
Expand Down Expand Up @@ -235,6 +240,19 @@ class LLMQ
int32_t m_expiry_height{0};
};
virtual std::vector<QuorumInfo> getQuorumStats() = 0;
struct PlatformQuorum {
uint256 m_quorum_hash{};
std::vector<uint8_t> 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<PlatformQuorum> 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<uint8_t> getInstantSendLock(const uint256& txid) = 0;
virtual void setContext(node::NodeContext* context) {}
};

Expand Down
74 changes: 74 additions & 0 deletions src/node/interfaces.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
#include <interfaces/handler.h>
#include <interfaces/wallet.h>
#include <kernel/chain.h>
#include <llmq/blockprocessor.h>
#include <llmq/commitment.h>
#include <llmq/context.h>
#include <llmq/options.h>
Expand Down Expand Up @@ -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<CService> getPlatformHTTPSAddrs() const override
{
std::vector<CService> 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)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include legacy EvoNode HTTPS endpoints

For EvoNodes whose state version predates ProTxVersion::ExtAddr, the Platform endpoint is stored as platformHTTPPort alongside the primary Core address rather than as a PLATFORM_HTTPS netInfo entry (src/evo/dmnstate.h:112-115, with the conversion spelled out in src/evo/specialtxman.cpp:471-473). This loop therefore returns an empty vector for valid legacy EvoNodes that have not submitted an extended-address update, removing those DAPI gateways from the GUI; synthesize the service from the primary address and legacy HTTP port when netInfo cannot store Platform entries.

AGENTS.md reference: AGENTS.md:L15-L17

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid — fixed at the new head.

Confirmed against the code: CDeterministicMNState only serializes platformP2PPort/platformHTTPPort when nVersion < ProTxVersion::ExtAddr (src/evo/dmnstate.h:112-115), and specialtxman.cpp:471-478 zeroes those scalars from ExtAddr onward precisely because netInfo owns them then. So the netInfo-only loop returned an empty vector for every legacy EvoNode — which today is most of them, meaning the GUI would have seen gateways only from nodes that had already submitted an extended-address update.

getPlatformHTTPSAddrs() now branches on the state version: below ExtAddr it synthesizes the gateway from the primary address and the legacy platformHTTPPort (guarded on MnType::Evo and a non-zero port, using the same CService(addr, port) idiom as dmnstate.cpp:138 and rpc/evo_util.h:41); at ExtAddr and above it reads the PLATFORM_HTTPS netInfo entries as before.


🤖 Posted autonomously by Claude on behalf of pasta.

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; }
Expand Down Expand Up @@ -585,6 +607,58 @@ class LLMQImpl : public LLMQ
}
return stats;
}
std::vector<PlatformQuorum> getPlatformQuorums(uint8_t llmq_type) override
{
std::vector<PlatformQuorum> 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<Consensus::LLMQType>(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<size_t>(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<uint8_t> getInstantSendLock(const uint256& txid) override
{
if (!context().llmq_ctx || !context().llmq_ctx->isman) {
return {};
}
const auto islock{context().llmq_ctx->isman->GetInstantSendLockByTxid(txid)};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Read retained islocks even when InstantSend is disabled

When SPORK_2 is disabled, or the node is reindexing/importing, this call returns empty for every txid because CInstantSendManager::GetInstantSendLockByTxid() exits before querying its database (src/instantsend/instantsend.cpp:357-363, with the gate defined at lines 479-482). Consequently, a previously stored islock becomes unavailable to the Platform GUI precisely as it tries to build an asset-lock proof, despite this interface promising emptiness only when the transaction has no islock; use a retained-lock lookup that is independent of whether new InstantSend processing is enabled.

AGENTS.md reference: AGENTS.md:L169-L172

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is fine; no action needed imo

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;
Expand Down
30 changes: 2 additions & 28 deletions src/test/evo_cbtx_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
#include <chain.h>
#include <chainlock/chainlock.h>
#include <chainparams.h>
#include <compat/endian.h>
#include <consensus/merkle.h>
#include <consensus/validation.h>
#include <evo/cbtx.h>
Expand All @@ -28,8 +27,6 @@

#include <cstdint>
#include <limits>
#include <string>
#include <tuple>
#include <utility>
#include <vector>

Expand Down Expand Up @@ -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<std::string, Consensus::LLMQType, uint32_t> BuildInversedHeightKey(Consensus::LLMQType llmqType, int nMinedHeight)
{
return std::make_tuple(DB_MINED_COMMITMENT_BY_INVERSED_HEIGHT, llmqType,
htobe32_internal(std::numeric_limits<uint32_t>::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;
Expand Down Expand Up @@ -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();
}

Expand All @@ -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();
}

Expand Down
154 changes: 154 additions & 0 deletions src/test/evo_deterministicmns_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include <evo/simplifiedmns.h>
#include <evo/specialtx.h>
#include <evo/specialtxman.h>
#include <interfaces/node.h>
#include <llmq/context.h>
#include <node/mempool_args.h>
#include <messagesigner.h>
Expand Down Expand Up @@ -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<uint256, interfaces::MnEntryCPtr> 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<CNetAddr>(addrs[0]) == static_cast<CNetAddr>(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
Expand Down
Loading
Loading