Skip to content
Open
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
5 changes: 5 additions & 0 deletions src/evo/netinfo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,11 @@ NetInfoStatus ExtNetInfo::Validate() const
// Purpose if present in map must have at least one entry
return NetInfoStatus::Malformed;
}
if (entries.size() > MAX_ENTRIES_EXTNETINFO) {
// Per-purpose entry count is bounded at insertion; a deserialized object
// may exceed it and must be rejected here
return NetInfoStatus::MaxLimit;
}
if (HasAddrDuplicates(entries)) {
return NetInfoStatus::Duplicate;
}
Expand Down
6 changes: 4 additions & 2 deletions src/evo/specialtxman.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -883,11 +883,13 @@ static bool CheckService(const ProTx& proTx, TxValidationState& state)
return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-bad");
case NetInfoStatus::Success:
return true;
// Shouldn't be possible during self-checks
// 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:
assert(false);
return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-maxlimit");
} // no default case, so the compiler can warn about missing cases
assert(false);
}
Expand Down
96 changes: 96 additions & 0 deletions src/test/evo_deterministicmns_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include <test/util/setup_common.h>

#include <chainparams.h>
#include <clientversion.h>
#include <consensus/validation.h>
#include <deploymentstatus.h>
#include <evo/deterministicmns.h>
Expand All @@ -14,12 +15,14 @@
#include <evo/specialtxman.h>
#include <llmq/context.h>
#include <messagesigner.h>
#include <netbase.h>
#include <node/transaction.h>
#include <policy/policy.h>
#include <script/interpreter.h>
#include <script/sign.h>
#include <script/signingprovider.h>
#include <script/standard.h>
#include <streams.h>
#include <test/util/txmempool.h>
#include <txmempool.h>
#include <validation.h>
Expand Down Expand Up @@ -665,6 +668,93 @@ void FuncProUpRegTxV2CannotBypassV4PayoutCollateralReuse(TestChainSetup& setup)
BOOST_CHECK_EQUAL(val_state.GetRejectReason(), "bad-protx-payee-reuse");
}

static std::shared_ptr<NetInfoInterface> DeserializeCoreP2PExtNetInfo(const std::vector<NetInfoEntry>& entries)
{
// Hand-roll ExtNetInfo's serialization (version byte followed by a map of purpose to entries)
// to construct states that AddEntry() would refuse to produce
CDataStream ds(SER_DISK, CLIENT_VERSION);
ds << uint8_t{1}; // m_version (ExtNetInfo::CURRENT_VERSION)
WriteCompactSize(ds, 1); // m_data map size (one purpose)
ds << NetInfoPurpose::CORE_P2P; // map key (purpose)
WriteCompactSize(ds, entries.size()); // entry count for this purpose
for (const auto& entry : entries) {
ds << entry;
}

auto net_info{std::make_shared<ExtNetInfo>()};
ds >> *net_info;
return net_info;
}

static CTransaction BuildExtNetInfoProRegTx(std::shared_ptr<NetInfoInterface> net_info)
{
CKey owner_key;
owner_key.MakeNewKey(true);
CBLSSecretKey operator_key;
operator_key.MakeNewKey();

CProRegTx pro_reg;
pro_reg.nVersion = ProTxVersion::ExtAddr;
pro_reg.netInfo = std::move(net_info);
pro_reg.keyIDOwner = owner_key.GetPubKey().GetID();
pro_reg.pubKeyOperator.Set(operator_key.GetPublicKey(), bls::bls_legacy_scheme.load());
pro_reg.keyIDVoting = owner_key.GetPubKey().GetID();
pro_reg.scriptPayout = GenerateRandomAddress();

CMutableTransaction tx;
tx.nVersion = 3;
tx.nType = TRANSACTION_PROVIDER_REGISTER;
pro_reg.inputsHash = CalcTxInputsHash(CTransaction(tx));
SetTxPayload(tx, pro_reg);
return CTransaction(tx);
}

void FuncProRegTxRejectsInvalidDeserializedExtNetInfo(TestChainSetup& setup)
{
auto& chainman = *Assert(setup.m_node.chainman.get());
auto& dmnman = *Assert(setup.m_node.dmnman);
const CScript coinbase_pk = GetScriptForRawPubKey(setup.coinbaseKey.GetPubKey());

for (int i = 0; i < 2000 && !DeploymentActiveAfter(chainman.ActiveChain().Tip(), chainman, Consensus::DEPLOYMENT_V24); ++i) {
setup.CreateAndProcessBlock({}, coinbase_pk);
dmnman.UpdatedBlockTip(chainman.ActiveChain().Tip());
}
BOOST_REQUIRE(DeploymentActiveAfter(chainman.ActiveChain().Tip(), chainman.GetConsensus(), Consensus::DEPLOYMENT_V19));
BOOST_REQUIRE(DeploymentActiveAfter(chainman.ActiveChain().Tip(), chainman, Consensus::DEPLOYMENT_V24));
BOOST_REQUIRE(!bls::bls_legacy_scheme.load());

auto check_reject_reason = [&](std::shared_ptr<NetInfoInterface> net_info, const std::string& reject_reason) {
TxValidationState state;
{
LOCK(cs_main);
BOOST_CHECK(!CheckProRegTx(BuildExtNetInfoProRegTx(std::move(net_info)), chainman.ActiveChain().Tip(),
dmnman, chainman.ActiveChainstate().CoinsTip(), chainman, state,
/*check_sigs=*/false));
}
BOOST_CHECK_EQUAL(state.GetResult(), TxValidationResult::TX_BAD_SPECIAL);
BOOST_CHECK_EQUAL(state.GetRejectReason(), reject_reason);
};
Comment on lines +726 to +736

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

💬 Nitpick: Also assert TxValidationResult::TX_BAD_SPECIAL in check_reject_reason

The fix in 5ba9ace deliberately maps BadInput/Duplicate/MaxLimit to TX_BAD_SPECIAL with stable reason strings. The new helper only checks the reject reason, so a future regression that keeps the same reason string but degrades the result class (e.g. TX_CONSENSUS or TX_RESULT_UNSET) would slip through. Since this test exists specifically to lock down both halves of that mapping, adding a result-class assertion costs one line and tightens the guarantee.

Suggested change
auto check_reject_reason = [&](std::shared_ptr<NetInfoInterface> net_info, const std::string& reject_reason) {
TxValidationState state;
{
LOCK(cs_main);
BOOST_CHECK(!CheckProRegTx(BuildExtNetInfoProRegTx(std::move(net_info)), chainman.ActiveChain().Tip(),
dmnman, chainman.ActiveChainstate().CoinsTip(), chainman, state,
/*check_sigs=*/false));
}
BOOST_CHECK_EQUAL(state.GetRejectReason(), reject_reason);
};
auto check_reject_reason = [&](std::shared_ptr<NetInfoInterface> net_info, const std::string& reject_reason) {
TxValidationState state;
{
LOCK(cs_main);
BOOST_CHECK(!CheckProRegTx(BuildExtNetInfoProRegTx(std::move(net_info)), chainman.ActiveChain().Tip(),
dmnman, chainman.ActiveChainstate().CoinsTip(), chainman, state,
/*check_sigs=*/false));
}
BOOST_CHECK_EQUAL(state.GetResult(), TxValidationResult::TX_BAD_SPECIAL);
BOOST_CHECK_EQUAL(state.GetRejectReason(), reject_reason);
};

source: ['coderabbit']


const uint16_t port{9998};
const NetInfoEntry p2p_entry{LookupNumeric("1.1.1.1", port)};
BOOST_REQUIRE(p2p_entry.IsTriviallyValid());
check_reject_reason(DeserializeCoreP2PExtNetInfo({p2p_entry, p2p_entry}), "bad-protx-dup-netinfo-entry");

std::vector<NetInfoEntry> too_many_entries;
for (size_t i = 1; i <= MAX_ENTRIES_EXTNETINFO + 1; ++i) {
const NetInfoEntry entry{LookupNumeric(strprintf("1.1.1.%d", i), port)};
BOOST_REQUIRE(entry.IsTriviallyValid());
too_many_entries.emplace_back(entry);
}
check_reject_reason(DeserializeCoreP2PExtNetInfo(too_many_entries), "bad-protx-netinfo-maxlimit");

DomainPort domain;
BOOST_REQUIRE_EQUAL(domain.Set("example.com", 443), DomainPort::Status::Success);
const NetInfoEntry domain_entry{domain};
BOOST_REQUIRE(domain_entry.IsTriviallyValid());
check_reject_reason(DeserializeCoreP2PExtNetInfo({domain_entry}), "bad-protx-netinfo-entry");
}

void FuncDIP3Protx(TestChainSetup& setup)
{
auto& chainman = *Assert(setup.m_node.chainman.get());
Expand Down Expand Up @@ -1268,6 +1358,12 @@ BOOST_AUTO_TEST_CASE(proupreg_v2_cannot_bypass_v4_payout_collateral_reuse)
FuncProUpRegTxV2CannotBypassV4PayoutCollateralReuse(setup);
}

BOOST_AUTO_TEST_CASE(proregtx_rejects_invalid_deserialized_extnetinfo)
{
TestChainV24SignalBeforeV19Setup setup;
FuncProRegTxRejectsInvalidDeserializedExtNetInfo(setup);
}

BOOST_AUTO_TEST_CASE(dip3_protx_legacy)
{
TestChainDIP3Setup setup;
Expand Down
63 changes: 63 additions & 0 deletions src/test/evo_netinfo_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -497,4 +497,67 @@ BOOST_AUTO_TEST_CASE(domainport_rules)
}
}

BOOST_FIXTURE_TEST_CASE(extnetinfo_validate_deser, RegTestingSetup)
{
// The in-memory builder (AddEntry/ProcessCandidate) refuses duplicates, domains
// on non-HTTPS purposes, and lists exceeding MAX_ENTRIES_EXTNETINFO. None of those
// checks run on deserialization, so Validate() must catch them itself.
const uint16_t port{9998};

auto write_header = [](CDataStream& ds, size_t n_purposes) {
ds << uint8_t{1}; // current version
WriteCompactSize(ds, n_purposes);
};

{
// Two identical entries under the same purpose
CDataStream ds(SER_DISK, CLIENT_VERSION);
write_header(ds, 1);
ds << NetInfoPurpose::CORE_P2P;
WriteCompactSize(ds, 2);
const NetInfoEntry entry{LookupNumeric("1.1.1.1", port)};
BOOST_CHECK(entry.IsTriviallyValid());
ds << entry << entry;

ExtNetInfo netInfo;
ds >> netInfo;
BOOST_CHECK_EQUAL(netInfo.Validate(), NetInfoStatus::Duplicate);
}

{
// List exceeds MAX_ENTRIES_EXTNETINFO per purpose
CDataStream ds(SER_DISK, CLIENT_VERSION);
write_header(ds, 1);
ds << NetInfoPurpose::CORE_P2P;
WriteCompactSize(ds, MAX_ENTRIES_EXTNETINFO + 1);
for (size_t i = 1; i <= MAX_ENTRIES_EXTNETINFO + 1; ++i) {
const NetInfoEntry entry{LookupNumeric(strprintf("1.1.1.%d", i), port)};
BOOST_CHECK(entry.IsTriviallyValid());
ds << entry;
}

ExtNetInfo netInfo;
ds >> netInfo;
BOOST_CHECK_EQUAL(netInfo.Validate(), NetInfoStatus::MaxLimit);
}

{
// Domain entry under a non-PLATFORM_HTTPS purpose
DomainPort domain;
BOOST_CHECK_EQUAL(domain.Set("example.com", 443), DomainPort::Status::Success);
const NetInfoEntry entry{domain};
BOOST_CHECK(entry.IsTriviallyValid());

CDataStream ds(SER_DISK, CLIENT_VERSION);
write_header(ds, 1);
ds << NetInfoPurpose::CORE_P2P;
WriteCompactSize(ds, 1);
ds << entry;

ExtNetInfo netInfo;
ds >> netInfo;
BOOST_CHECK_EQUAL(netInfo.Validate(), NetInfoStatus::BadInput);
}
}

BOOST_AUTO_TEST_SUITE_END()
Loading