Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
3 changes: 3 additions & 0 deletions src/evo/providertx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,9 @@ bool CProUpServTx::IsTriviallyValid(TxValidationState& state) const
if (nVersion < ProTxVersion::BasicBLS && nType == MnType::Evo) {
return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-protx-evo-version");
}
if (!IsValidMnType(nType)) {
return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-protx-type");
}
if (netInfo->CanStorePlatform() != (nVersion >= ProTxVersion::ExtAddr)) {
return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-protx-netinfo-version");
}
Expand Down
9 changes: 9 additions & 0 deletions src/evo/specialtxman.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1187,6 +1187,15 @@ bool CheckProUpServTx(const CTransaction& tx, gsl::not_null<const CBlockIndex*>
return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-protx-hash");
}

// Mirror BuildNewListFromBlock: nType must be valid and must match the registered MN.
// Without these checks a mempool-accepted ProUpServTx can make CreateNewBlock fail.
if (opt_ptx->nType != dmn->nType) {
return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-protx-type-mismatch");
}
if (!IsValidMnType(opt_ptx->nType)) {
return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-protx-type");
}

if (!IsVersionChangeValid(pindexPrev, dmn->pdmnState->nVersion, opt_ptx->nVersion, chainman, state)) {
// pass the state returned by the function above
return false;
Expand Down
136 changes: 136 additions & 0 deletions src/test/evo_deterministicmns_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <consensus/merkle.h>
#include <consensus/validation.h>
#include <deploymentstatus.h>
#include <evo/chainhelper.h>
#include <evo/deterministicmns.h>
#include <evo/providertx.h>
#include <evo/simplifiedmns.h>
Expand All @@ -27,6 +28,7 @@
#include <streams.h>
#include <test/util/txmempool.h>
#include <txmempool.h>
#include <util/std23.h>
#include <validation.h>

#include <boost/test/unit_test.hpp>
Expand Down Expand Up @@ -1190,6 +1192,132 @@ void FuncDIP3Protx(TestChainSetup& setup)
const_cast<Consensus::Params&>(Params().GetConsensus()).DIP0003EnforcementHeight = DIP0003EnforcementHeightBackup;
}

// ProUpServTx with an out-of-range or mismatched nType must be rejected before
// mempool acceptance. Historically IsTriviallyValid / CheckProUpServTx ignored nType while
// BuildNewListFromBlock rejected it, so a single relayed tx stalled CreateNewBlock/getblocktemplate.
void FuncProUpServInvalidNType(TestChainSetup& setup)
{
auto& chainman = *Assert(setup.m_node.chainman.get());
auto& dmnman = *Assert(setup.m_node.dmnman);
auto tip_index = [&] { return WITH_LOCK(::cs_main, return chainman.ActiveChain().Tip()); };
auto sync_dmn_tip = [&] { dmnman.UpdatedBlockTip(tip_index()); };

auto utxos = BuildSimpleUtxoMap(setup.m_coinbase_txns);
const CScript coinbase_pk = GetScriptForRawPubKey(setup.coinbaseKey.GetPubKey());

CKey ownerKey;
CBLSSecretKey operatorKey;
auto tx_reg = CreateProRegTx(chainman, utxos, /*port=*/1, GenerateRandomAddress(), setup.coinbaseKey, ownerKey,
operatorKey);
const uint256 proTxHash = tx_reg.GetHash();
setup.CreateAndProcessBlock({tx_reg}, coinbase_pk);
sync_dmn_tip();
BOOST_REQUIRE(dmnman.GetListAtChainTip().HasMN(proTxHash));
BOOST_REQUIRE_EQUAL(std23::to_underlying(dmnman.GetListAtChainTip().GetMN(proTxHash)->nType),
std23::to_underlying(MnType::Regular));

auto make_bad_proupserv = [&](MnType nType) {
CProUpServTx proTx;
proTx.nVersion = ProTxVersion::GetMax(!bls::bls_legacy_scheme, /*is_extended_addr=*/false);
proTx.nType = nType;
proTx.netInfo = NetInfoInterface::MakeNetInfo(proTx.nVersion);
proTx.proTxHash = proTxHash;
BOOST_REQUIRE_EQUAL(proTx.netInfo->AddEntry(NetInfoPurpose::CORE_P2P, "1.1.1.1:42"), NetInfoStatus::Success);
if (nType == MnType::Evo) {
// Platform fields required by CheckProUpServTx when nType claims Evo.
proTx.platformNodeID.SetHex("00112233445566778899aabbccddeeff00112233");
proTx.platformP2PPort = 20001;
proTx.platformHTTPPort = 20002;
}

CMutableTransaction tx;
tx.nVersion = 3;
tx.nType = TRANSACTION_PROVIDER_UPDATE_SERVICE;
const auto spent =
FundTransaction(chainman, tx, utxos, GetScriptForDestination(PKHash(setup.coinbaseKey.GetPubKey())), 1 * COIN);
// FundTransaction constructs a zero-fee tx; leave a dust-sized fee so ProcessTransaction
// reaches special-tx validation instead of failing on min-relay fee first.
BOOST_REQUIRE(!tx.vout.empty());
BOOST_REQUIRE(tx.vout[0].nValue > 10000);
tx.vout[0].nValue -= 10000;
proTx.inputsHash = CalcTxInputsHash(CTransaction(tx));
proTx.sig = operatorKey.Sign(::SerializeHash(proTx), bls::bls_legacy_scheme);
SetTxPayload(tx, proTx);
SignTransaction(tx, spent, setup.coinbaseKey);
return tx;
};

// Case 1: out-of-range nType. Must fail trivial validation and never enter the mempool.
{
auto tx = make_bad_proupserv(static_cast<MnType>(42));
auto opt_payload = GetTxPayload<CProUpServTx>(tx, /*assert_type=*/false);
BOOST_REQUIRE(opt_payload.has_value());

TxValidationState trivial_state;
BOOST_CHECK(!opt_payload->IsTriviallyValid(trivial_state));
BOOST_CHECK_EQUAL(trivial_state.GetRejectReason(), "bad-protx-type");

TxValidationState check_state;
{
LOCK(cs_main);
BOOST_CHECK(!CheckProUpServTx(CTransaction(tx), tip_index(), dmnman, chainman, check_state, /*check_sigs=*/true));
}
BOOST_CHECK_EQUAL(check_state.GetRejectReason(), "bad-protx-type");

{
LOCK(cs_main);
const MempoolAcceptResult result = chainman.ProcessTransaction(MakeTransactionRef(tx));
BOOST_CHECK(result.m_result_type == MempoolAcceptResult::ResultType::INVALID);
BOOST_CHECK_EQUAL(result.m_state.GetRejectReason(), "bad-protx-type");
}
}

// Case 2: valid-range but mismatched nType (claim Evo for a Regular MN). Must fail contextual checks.
{
auto tx = make_bad_proupserv(MnType::Evo);
auto opt_payload = GetTxPayload<CProUpServTx>(tx, /*assert_type=*/false);
BOOST_REQUIRE(opt_payload.has_value());

// Trivial validation accepts Evo as a valid type; contextual validation must still reject.
TxValidationState trivial_state;
BOOST_CHECK(opt_payload->IsTriviallyValid(trivial_state));

TxValidationState check_state;
{
LOCK(cs_main);
BOOST_CHECK(!CheckProUpServTx(CTransaction(tx), tip_index(), dmnman, chainman, check_state, /*check_sigs=*/true));
}
BOOST_CHECK_EQUAL(check_state.GetRejectReason(), "bad-protx-type-mismatch");

{
LOCK(cs_main);
const MempoolAcceptResult result = chainman.ProcessTransaction(MakeTransactionRef(tx));
BOOST_CHECK(result.m_result_type == MempoolAcceptResult::ResultType::INVALID);
BOOST_CHECK_EQUAL(result.m_state.GetRejectReason(), "bad-protx-type-mismatch");
}
}

// Case 3: consensus path still rejects a hand-built block containing the bad tx (defense in depth).
{
auto tx = make_bad_proupserv(static_cast<MnType>(42));
CMutableTransaction coinbase;
coinbase.vin.emplace_back(COutPoint(), CScript() << OP_0 << OP_0);
coinbase.vout.emplace_back(50 * COIN, coinbase_pk);

CBlock block;
block.vtx.emplace_back(MakeTransactionRef(std::move(coinbase)));
block.vtx.emplace_back(MakeTransactionRef(tx));
BlockValidationState state;
CDeterministicMNList mn_list;
LOCK(cs_main);
BOOST_CHECK(!chainman.ActiveChainstate().ChainHelper().special_tx->BuildNewListFromBlock(
block, tip_index(), chainman.ActiveChainstate().CoinsTip(), /*debugLogs=*/false, state, mn_list));
// type-mismatch is evaluated before IsValidMnType in BuildNewListFromBlock
BOOST_CHECK(state.GetRejectReason() == "bad-protx-type-mismatch" ||
state.GetRejectReason() == "bad-protx-type");
}
}

void FuncTestMempoolReorg(TestChainSetup& setup)
{
auto& chainman = *Assert(setup.m_node.chainman.get());
Expand Down Expand Up @@ -1566,6 +1694,14 @@ BOOST_AUTO_TEST_CASE(dip3_protx_basic)
FuncDIP3Protx(setup);
}

// Requires BasicBLS so ProUpServTx serialises nType. Pre-fix this test fails because
// IsTriviallyValid / CheckProUpServTx accept out-of-range nType.
BOOST_AUTO_TEST_CASE(proupserv_invalid_ntype_basic)
{
TestChainV19Setup setup;
FuncProUpServInvalidNType(setup);
}

BOOST_AUTO_TEST_CASE(test_mempool_reorg_legacy)
{
TestChainDIP3Setup setup;
Expand Down
60 changes: 60 additions & 0 deletions src/test/evo_trivialvalidation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,20 @@
#include <test/util/json.h>
#include <test/util/setup_common.h>

#include <bls/bls.h>
#include <chain.h>
#include <chainparams.h>
#include <deploymentstatus.h>
#include <evo/dmn_types.h>
#include <evo/netinfo.h>
#include <evo/providertx.h>
#include <evo/specialtxman.h>
#include <key.h>
#include <primitives/transaction.h>
#include <script/script.h>
#include <script/standard.h>
#include <util/check.h>
#include <util/std23.h>
#include <validation.h>

#include <boost/test/unit_test.hpp>
Expand Down Expand Up @@ -199,4 +204,59 @@ BOOST_AUTO_TEST_CASE(multipayout_list_validation)
BOOST_CHECK_EQUAL(state.GetRejectReason(), "bad-protx-payee-reuse");
}

// Regression: CProUpServTx::IsTriviallyValid must reject out-of-range nType,
// matching CProRegTx::IsTriviallyValid and the block-connect check in BuildNewListFromBlock.
// Without this, a ProUpServTx with nType >= MnType::COUNT is mempool-valid but block-invalid,
// which makes CreateNewBlock/getblocktemplate throw once the tx is relayed.
BOOST_AUTO_TEST_CASE(proupserv_rejects_invalid_ntype)
{
auto make_proupserv = [](MnType nType) {
CProUpServTx proTx;
proTx.nVersion = ProTxVersion::BasicBLS;
proTx.nType = nType;
proTx.netInfo = NetInfoInterface::MakeNetInfo(proTx.nVersion);
// Avoid the mainnet default port (9999); regtest rejects it for CORE_P2P.
BOOST_REQUIRE_EQUAL(proTx.netInfo->AddEntry(NetInfoPurpose::CORE_P2P, "1.1.1.1:1000"),
NetInfoStatus::Success);
return proTx;
};

// Valid types must still pass trivial validation.
{
TxValidationState state;
BOOST_CHECK(make_proupserv(MnType::Regular).IsTriviallyValid(state));
state = {};
BOOST_CHECK(make_proupserv(MnType::Evo).IsTriviallyValid(state));
}

// Out-of-range nType (raw uint16 deserialised into the enum) must be rejected.
for (const MnType bad_type : {static_cast<MnType>(2), static_cast<MnType>(42), static_cast<MnType>(0xFFFF)}) {
TxValidationState state;
BOOST_CHECK_MESSAGE(!make_proupserv(bad_type).IsTriviallyValid(state),
"nType=" << std23::to_underlying(bad_type) << " should be rejected");
BOOST_CHECK_EQUAL(state.GetRejectReason(), "bad-protx-type");
}

// ProRegTx already has the same guard; keep the two special-tx types aligned.
{
CProRegTx proReg;
proReg.nVersion = ProTxVersion::BasicBLS;
proReg.nType = static_cast<MnType>(42);
proReg.netInfo = NetInfoInterface::MakeNetInfo(proReg.nVersion);
CKey owner_key, voting_key, payout_key;
owner_key.MakeNewKey(true);
voting_key.MakeNewKey(true);
proReg.keyIDOwner = owner_key.GetPubKey().GetID();
proReg.keyIDVoting = voting_key.GetPubKey().GetID();
proReg.scriptPayout = NewP2PKHScript(payout_key);
CBLSSecretKey operator_key;
operator_key.MakeNewKey();
proReg.pubKeyOperator.Set(operator_key.GetPublicKey(), /*legacy=*/false);

TxValidationState state;
BOOST_CHECK(!proReg.IsTriviallyValid(state));
BOOST_CHECK_EQUAL(state.GetRejectReason(), "bad-protx-type");
}
}

BOOST_AUTO_TEST_SUITE_END()