Skip to content
Draft
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
3 changes: 3 additions & 0 deletions src/evo/providertx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,9 @@ bool CProUpServTx::IsTriviallyValid(gsl::not_null<const CBlockIndex*> pindexPrev
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 @@ -1185,6 +1185,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, tx.nType, 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(tip_index(), chainman, 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(tip_index(), chainman, 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
64 changes: 64 additions & 0 deletions src/test/evo_trivialvalidation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,19 @@
#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 <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 @@ -200,4 +205,63 @@ 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& chainman = *Assert(m_node.chainman.get());
const CBlockIndex* pindexPrev{WITH_LOCK(::cs_main, return chainman.ActiveChain().Tip())};
BOOST_REQUIRE(pindexPrev);

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(pindexPrev, chainman, state));
state = {};
BOOST_CHECK(make_proupserv(MnType::Evo).IsTriviallyValid(pindexPrev, chainman, 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(pindexPrev, chainman, 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(pindexPrev, chainman, state));
BOOST_CHECK_EQUAL(state.GetRejectReason(), "bad-protx-type");
}
}

BOOST_AUTO_TEST_SUITE_END()
Loading