From 89089ed281f0b8a60f7585bc2dffafe2674976c4 Mon Sep 17 00:00:00 2001 From: Hilawe Semunegus Date: Sat, 6 Jun 2026 01:01:01 -0400 Subject: [PATCH 01/15] DIP-0026 P1: add ProTx v4 (MultiPayout) + DEPLOYMENT_V25 activation gate Scaffolding for DIP-0026 multi-party masternode payouts. Introduces a new special-transaction version ProTxVersion::MultiPayout (v4) for CProRegTx and CProUpRegTx, gated behind a new EHF-style hard-fork deployment DEPLOYMENT_V25. No payout-share fields or payout logic yet: a v4 ProTx is rejected until the fork activates, so there is no behavior change on existing networks. - consensus/params.h, deploymentinfo.cpp: register DEPLOYMENT_V25 ("v25"). - chainparams.cpp: V25 params for all four networks (bit 13, EHF); NEVER_ACTIVE on main/testnet/devnet, activatable on regtest. - evo/providertx.h: ProTxVersion::MultiPayout=4; extend GetMax() with an is_multi_payout axis; compile-time static_asserts pin the version tiers. - evo/providertx.cpp: GetMaxFromDeployment gates v4 on DEPLOYMENT_V25 for ProRegTx/ProUpRegTx only, and enforces V24-before-V25 for ProRegTx so a v4 reg-tx can never outrun extended-address support. Fix the CanStorePlatform netinfo-version gate (== ExtAddr -> >= ExtAddr) so v4 keeps extended netInfo. - rpc/blockchain.cpp: report v25 in getblockchaininfo EHF softforks. - test: update GetMax call sites; add v25 to rpc_blockchain.py expected softforks. --- src/chainparams.cpp | 36 +++++++++++++++++ src/consensus/params.h | 1 + src/deploymentinfo.cpp | 4 ++ src/evo/providertx.cpp | 25 +++++++++--- src/evo/providertx.h | 42 +++++++++++++++----- src/rpc/blockchain.cpp | 1 + src/test/block_reward_reallocation_tests.cpp | 2 +- src/test/evo_deterministicmns_tests.cpp | 16 ++++---- src/test/evo_simplifiedmns_tests.cpp | 2 +- test/functional/rpc_blockchain.py | 11 +++++ 10 files changed, 113 insertions(+), 27 deletions(-) diff --git a/src/chainparams.cpp b/src/chainparams.cpp index d7d491ba83e0..e37a92905ec6 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -218,6 +218,15 @@ class CMainParams : public CChainParams { consensus.vDeployments[Consensus::DEPLOYMENT_V24].nFalloffCoeff = 5; // this corresponds to 10 periods consensus.vDeployments[Consensus::DEPLOYMENT_V24].useEHF = true; + consensus.vDeployments[Consensus::DEPLOYMENT_V25].bit = 13; + consensus.vDeployments[Consensus::DEPLOYMENT_V25].nStartTime = Consensus::BIP9Deployment::NEVER_ACTIVE; // TODO + consensus.vDeployments[Consensus::DEPLOYMENT_V25].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; // TODO + consensus.vDeployments[Consensus::DEPLOYMENT_V25].nWindowSize = 4032; + consensus.vDeployments[Consensus::DEPLOYMENT_V25].nThresholdStart = 3226; // 80% of 4032 + consensus.vDeployments[Consensus::DEPLOYMENT_V25].nThresholdMin = 2420; // 60% of 4032 + consensus.vDeployments[Consensus::DEPLOYMENT_V25].nFalloffCoeff = 5; // this corresponds to 10 periods + consensus.vDeployments[Consensus::DEPLOYMENT_V25].useEHF = true; + consensus.nMinimumChainWork = uint256S("0x00000000000000000000000000000000000000000000b9040746437784aaec47"); // 2471728 consensus.defaultAssumeValid = uint256S("0x000000000000001a19ad7270422a00f86123ea94e0b295a3a796d6861bd7b032"); // 2471728 @@ -419,6 +428,15 @@ class CTestNetParams : public CChainParams { consensus.vDeployments[Consensus::DEPLOYMENT_V24].nFalloffCoeff = 5; // this corresponds to 10 periods consensus.vDeployments[Consensus::DEPLOYMENT_V24].useEHF = true; + consensus.vDeployments[Consensus::DEPLOYMENT_V25].bit = 13; + consensus.vDeployments[Consensus::DEPLOYMENT_V25].nStartTime = Consensus::BIP9Deployment::NEVER_ACTIVE; // TODO + consensus.vDeployments[Consensus::DEPLOYMENT_V25].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; + consensus.vDeployments[Consensus::DEPLOYMENT_V25].nWindowSize = 100; + consensus.vDeployments[Consensus::DEPLOYMENT_V25].nThresholdStart = 80; // 80% of 100 + consensus.vDeployments[Consensus::DEPLOYMENT_V25].nThresholdMin = 60; // 60% of 100 + consensus.vDeployments[Consensus::DEPLOYMENT_V25].nFalloffCoeff = 5; // this corresponds to 10 periods + consensus.vDeployments[Consensus::DEPLOYMENT_V25].useEHF = true; + consensus.nMinimumChainWork = uint256S("0x000000000000000000000000000000000000000000000000036c8f738da818d2"); // 1400000 consensus.defaultAssumeValid = uint256S("0x000000541a23f9db7411cddbe50f9f1ebd4aa7108ebdcad62214753f648c0239"); // 1400000 @@ -594,6 +612,15 @@ class CDevNetParams : public CChainParams { consensus.vDeployments[Consensus::DEPLOYMENT_V24].nFalloffCoeff = 5; // this corresponds to 10 periods consensus.vDeployments[Consensus::DEPLOYMENT_V24].useEHF = true; + consensus.vDeployments[Consensus::DEPLOYMENT_V25].bit = 13; + consensus.vDeployments[Consensus::DEPLOYMENT_V25].nStartTime = Consensus::BIP9Deployment::NEVER_ACTIVE; // TODO + consensus.vDeployments[Consensus::DEPLOYMENT_V25].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; + consensus.vDeployments[Consensus::DEPLOYMENT_V25].nWindowSize = 120; + consensus.vDeployments[Consensus::DEPLOYMENT_V25].nThresholdStart = 96; // 80% of 120 + consensus.vDeployments[Consensus::DEPLOYMENT_V25].nThresholdMin = 72; // 60% of 120 + consensus.vDeployments[Consensus::DEPLOYMENT_V25].nFalloffCoeff = 5; // this corresponds to 10 periods + consensus.vDeployments[Consensus::DEPLOYMENT_V25].useEHF = true; + consensus.nMinimumChainWork = uint256{}; consensus.defaultAssumeValid = uint256{}; @@ -831,6 +858,15 @@ class CRegTestParams : public CChainParams { consensus.vDeployments[Consensus::DEPLOYMENT_V24].nFalloffCoeff = 5; // this corresponds to 10 periods consensus.vDeployments[Consensus::DEPLOYMENT_V24].useEHF = true; + consensus.vDeployments[Consensus::DEPLOYMENT_V25].bit = 13; + consensus.vDeployments[Consensus::DEPLOYMENT_V25].nStartTime = 0; + consensus.vDeployments[Consensus::DEPLOYMENT_V25].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; + consensus.vDeployments[Consensus::DEPLOYMENT_V25].nWindowSize = 250; + consensus.vDeployments[Consensus::DEPLOYMENT_V25].nThresholdStart = 250 / 5 * 4; // 80% of window size + consensus.vDeployments[Consensus::DEPLOYMENT_V25].nThresholdMin = 250 / 5 * 3; // 60% of window size + consensus.vDeployments[Consensus::DEPLOYMENT_V25].nFalloffCoeff = 5; // this corresponds to 10 periods + consensus.vDeployments[Consensus::DEPLOYMENT_V25].useEHF = true; + consensus.nMinimumChainWork = uint256{}; consensus.defaultAssumeValid = uint256{}; diff --git a/src/consensus/params.h b/src/consensus/params.h index 37b5c75f2896..06ee9a09ce3d 100644 --- a/src/consensus/params.h +++ b/src/consensus/params.h @@ -41,6 +41,7 @@ constexpr bool ValidDeployment(BuriedDeployment dep) { return dep <= DEPLOYMENT_ enum DeploymentPos : uint16_t { DEPLOYMENT_TESTDUMMY, DEPLOYMENT_V24, // Deployment of doubling withdrawal limit, extended addresses + DEPLOYMENT_V25, // Deployment of DIP0026 multi-party masternode payouts // NOTE: Also add new deployments to VersionBitsDeploymentInfo in deploymentinfo.cpp MAX_VERSION_BITS_DEPLOYMENTS }; diff --git a/src/deploymentinfo.cpp b/src/deploymentinfo.cpp index 856403e32889..0ce08a6e7186 100644 --- a/src/deploymentinfo.cpp +++ b/src/deploymentinfo.cpp @@ -15,6 +15,10 @@ const struct VBDeploymentInfo VersionBitsDeploymentInfo[Consensus::MAX_VERSION_B /*.name =*/"v24", /*.gbt_force =*/true, }, + { + /*.name =*/"v25", + /*.gbt_force =*/true, + }, }; std::string DeploymentName(Consensus::BuriedDeployment dep) diff --git a/src/evo/providertx.cpp b/src/evo/providertx.cpp index b2c3691aa4f5..bf23da96b842 100644 --- a/src/evo/providertx.cpp +++ b/src/evo/providertx.cpp @@ -21,10 +21,23 @@ template const ChainstateManager& chainman, std::optional is_basic_override) { constexpr bool is_extaddr_eligible{std::is_same_v, CProRegTx> || std::is_same_v, CProUpServTx>}; - return ProTxVersion::GetMax( - is_basic_override ? *is_basic_override - : DeploymentActiveAfter(pindexPrev, chainman.GetConsensus(), Consensus::DEPLOYMENT_V19), - is_extaddr_eligible ? DeploymentActiveAfter(pindexPrev, chainman, Consensus::DEPLOYMENT_V24) : false); + // DIP0026 multi-party payouts apply to the owner-side payout, which is only carried by + // ProRegTx and ProUpRegTx. ProUpServTx (operator payout) and ProUpRevTx are not eligible. + constexpr bool is_multipayout_eligible{std::is_same_v, CProRegTx> || std::is_same_v, CProUpRegTx>}; + + const bool is_basic{is_basic_override ? *is_basic_override + : DeploymentActiveAfter(pindexPrev, chainman.GetConsensus(), Consensus::DEPLOYMENT_V19)}; + const bool is_extaddr{is_extaddr_eligible && DeploymentActiveAfter(pindexPrev, chainman, Consensus::DEPLOYMENT_V24)}; + bool is_multipayout{is_multipayout_eligible && DeploymentActiveAfter(pindexPrev, chainman, Consensus::DEPLOYMENT_V25)}; + + // A v4 CProRegTx (MultiPayout > ExtAddr) implies extended-address netInfo, so multi-payout + // must never outrun the extended-address fork for an extaddr-eligible type. We enforce this + // in code rather than relying on chainparams ordering of V24/V25. CProUpRegTx carries no + // netInfo and is not extaddr-eligible, so it may reach v4 on DEPLOYMENT_V25 alone. + if (is_extaddr_eligible && is_multipayout && !is_extaddr) { + is_multipayout = false; + } + return ProTxVersion::GetMax(is_basic, is_extaddr, is_multipayout); } template uint16_t GetMaxFromDeployment(gsl::not_null pindexPrev, const ChainstateManager& chainman, @@ -89,7 +102,7 @@ bool CProRegTx::IsTriviallyValid(gsl::not_null pindexPrev, c if (!scriptPayout.IsPayToPublicKeyHash() && !scriptPayout.IsPayToScriptHash()) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-payee"); } - if (netInfo->CanStorePlatform() != (nVersion == ProTxVersion::ExtAddr)) { + if (netInfo->CanStorePlatform() != (nVersion >= ProTxVersion::ExtAddr)) { return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-protx-netinfo-version"); } if (!netInfo->IsEmpty() && !IsNetInfoTriviallyValid(*this, state)) { @@ -171,7 +184,7 @@ bool CProUpServTx::IsTriviallyValid(gsl::not_null pindexPrev if (nVersion < ProTxVersion::BasicBLS && nType == MnType::Evo) { return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-protx-evo-version"); } - if (netInfo->CanStorePlatform() != (nVersion == ProTxVersion::ExtAddr)) { + if (netInfo->CanStorePlatform() != (nVersion >= ProTxVersion::ExtAddr)) { return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-protx-netinfo-version"); } if (netInfo->IsEmpty()) { diff --git a/src/evo/providertx.h b/src/evo/providertx.h index 68c6fe3c7aa2..304908c0b324 100644 --- a/src/evo/providertx.h +++ b/src/evo/providertx.h @@ -27,19 +27,28 @@ struct RPCResult; namespace ProTxVersion { enum : uint16_t { - LegacyBLS = 1, - BasicBLS = 2, - ExtAddr = 3, + LegacyBLS = 1, + BasicBLS = 2, + ExtAddr = 3, + MultiPayout = 4, // DIP0026 multi-party payouts (gated by DEPLOYMENT_V25) }; /** Get highest permissible ProTx version based on flags set. */ -[[nodiscard]] constexpr uint16_t GetMax(const bool is_basic_scheme_active, const bool is_extended_addr) +[[nodiscard]] constexpr uint16_t GetMax(const bool is_basic_scheme_active, const bool is_extended_addr, + const bool is_multi_payout) { if (is_basic_scheme_active) { + // DIP0026 multi-party payouts (v4) build on top of basic BLS and are the highest + // version. For CProRegTx the extended-address (v3) features are implied because + // DEPLOYMENT_V25 only activates after DEPLOYMENT_V24; CProUpRegTx carries no netInfo + // and so transitions straight from v2 to v4. is_basic_scheme_active could be set to + // false due to RPC specialization, so it gates the whole block to avoid accidentally + // upgrading a legacy BLS node due to a later fork activation. + if (is_multi_payout) { + return ProTxVersion::MultiPayout; + } if (is_extended_addr) { - // Requires *both* forks to be active to use extended addresses. is_basic_scheme_active could - // be set to false due to RPC specialization, so we must evaluate is_extended_addr *last* to - // avoid accidentally upgrading a legacy BLS node to basic BLS due to v24 activation. + // Requires *both* forks to be active to use extended addresses. return ProTxVersion::ExtAddr; } return ProTxVersion::BasicBLS; @@ -47,6 +56,17 @@ enum : uint16_t { return ProTxVersion::LegacyBLS; } +// Compile-time verification of the version-tier logic (DIP0026). These guarantee the gating +// can never silently regress: basic-BLS gates everything, and multi-payout (v4) is the highest +// version, reachable with or without extended addresses (CProUpRegTx carries no netInfo so it +// goes v2 -> v4 directly; for CProRegTx, DEPLOYMENT_V25 only activates after DEPLOYMENT_V24). +static_assert(GetMax(/*basic=*/false, /*extaddr=*/false, /*multipayout=*/false) == LegacyBLS); +static_assert(GetMax(/*basic=*/false, /*extaddr=*/true, /*multipayout=*/true ) == LegacyBLS); +static_assert(GetMax(/*basic=*/true, /*extaddr=*/false, /*multipayout=*/false) == BasicBLS); +static_assert(GetMax(/*basic=*/true, /*extaddr=*/true, /*multipayout=*/false) == ExtAddr); +static_assert(GetMax(/*basic=*/true, /*extaddr=*/false, /*multipayout=*/true ) == MultiPayout); +static_assert(GetMax(/*basic=*/true, /*extaddr=*/true, /*multipayout=*/true ) == MultiPayout); + /** Get highest permissible ProTx version based on deployment status * Note: The override is needed because some RPCs need to use deployment status information for everything *except* * the BLS version upgrade since they are specializations for a specific BLS version. This is a one-off. @@ -84,7 +104,7 @@ class CProRegTx obj.nVersion ); if (obj.nVersion == 0 || - obj.nVersion > ProTxVersion::GetMax(/*is_basic_scheme_active=*/true, /*is_extended_addr=*/true)) { + obj.nVersion > ProTxVersion::GetMax(/*is_basic_scheme_active=*/true, /*is_extended_addr=*/true, /*is_multi_payout=*/true)) { // unknown version, bail out early return; } @@ -151,7 +171,7 @@ class CProUpServTx obj.nVersion ); if (obj.nVersion == 0 || - obj.nVersion > ProTxVersion::GetMax(/*is_basic_scheme_active=*/true, /*is_extended_addr=*/true)) { + obj.nVersion > ProTxVersion::GetMax(/*is_basic_scheme_active=*/true, /*is_extended_addr=*/true, /*is_multi_payout=*/true)) { // unknown version, bail out early return; } @@ -211,7 +231,7 @@ class CProUpRegTx obj.nVersion ); if (obj.nVersion == 0 || - obj.nVersion > ProTxVersion::GetMax(/*is_basic_scheme_active=*/true, /*is_extended_addr=*/true)) { + obj.nVersion > ProTxVersion::GetMax(/*is_basic_scheme_active=*/true, /*is_extended_addr=*/true, /*is_multi_payout=*/true)) { // unknown version, bail out early return; } @@ -265,7 +285,7 @@ class CProUpRevTx obj.nVersion ); if (obj.nVersion == 0 || - obj.nVersion > ProTxVersion::GetMax(/*is_basic_scheme_active=*/true, /*is_extended_addr=*/true)) { + obj.nVersion > ProTxVersion::GetMax(/*is_basic_scheme_active=*/true, /*is_extended_addr=*/true, /*is_multi_payout=*/true)) { // unknown version, bail out early return; } diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index eca003933174..05eee0e5964a 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -1609,6 +1609,7 @@ RPCHelpMan getblockchaininfo() } for (auto ehf_deploy : { /* sorted by activation block */ Consensus::DEPLOYMENT_V24, + Consensus::DEPLOYMENT_V25, Consensus::DEPLOYMENT_TESTDUMMY }) { SoftForkDescPushBack(&tip, ehfSignals, softforks, chainman, ehf_deploy); } diff --git a/src/test/block_reward_reallocation_tests.cpp b/src/test/block_reward_reallocation_tests.cpp index 97c29c62c9d0..dca62e913e89 100644 --- a/src/test/block_reward_reallocation_tests.cpp +++ b/src/test/block_reward_reallocation_tests.cpp @@ -115,7 +115,7 @@ static CMutableTransaction CreateProRegTx(const CChain& active_chain, const CTxM operatorKeyRet.MakeNewKey(); CProRegTx proTx; - proTx.nVersion = ProTxVersion::GetMax(!bls::bls_legacy_scheme, /*is_extended_addr=*/false); + proTx.nVersion = ProTxVersion::GetMax(!bls::bls_legacy_scheme, /*is_extended_addr=*/false, /*is_multi_payout=*/false); proTx.netInfo = NetInfoInterface::MakeNetInfo(proTx.nVersion); proTx.collateralOutpoint.n = 0; BOOST_CHECK_EQUAL(proTx.netInfo->AddEntry(NetInfoPurpose::CORE_P2P, strprintf("1.1.1.1:%d", port)), diff --git a/src/test/evo_deterministicmns_tests.cpp b/src/test/evo_deterministicmns_tests.cpp index af286a51f148..bde861d511b1 100644 --- a/src/test/evo_deterministicmns_tests.cpp +++ b/src/test/evo_deterministicmns_tests.cpp @@ -106,7 +106,7 @@ static CMutableTransaction CreateProRegTx(const CChain& active_chain, const CTxM operatorKeyRet.MakeNewKey(); CProRegTx proTx; - proTx.nVersion = ProTxVersion::GetMax(!bls::bls_legacy_scheme, /*is_extended_addr=*/false); + proTx.nVersion = ProTxVersion::GetMax(!bls::bls_legacy_scheme, /*is_extended_addr=*/false, /*is_multi_payout=*/false); proTx.netInfo = NetInfoInterface::MakeNetInfo(proTx.nVersion); proTx.collateralOutpoint.n = 0; BOOST_CHECK_EQUAL(proTx.netInfo->AddEntry(NetInfoPurpose::CORE_P2P, strprintf("1.1.1.1:%d", port)), @@ -130,7 +130,7 @@ static CMutableTransaction CreateProRegTx(const CChain& active_chain, const CTxM static CMutableTransaction CreateProUpServTx(const CChain& active_chain, const CTxMemPool& mempool, SimpleUTXOMap& utxos, const uint256& proTxHash, const CBLSSecretKey& operatorKey, int port, const CScript& scriptOperatorPayout, const CKey& coinbaseKey) { CProUpServTx proTx; - proTx.nVersion = ProTxVersion::GetMax(!bls::bls_legacy_scheme, /*is_extended_addr=*/false); + proTx.nVersion = ProTxVersion::GetMax(!bls::bls_legacy_scheme, /*is_extended_addr=*/false, /*is_multi_payout=*/false); proTx.netInfo = NetInfoInterface::MakeNetInfo(proTx.nVersion); proTx.proTxHash = proTxHash; BOOST_CHECK_EQUAL(proTx.netInfo->AddEntry(NetInfoPurpose::CORE_P2P, strprintf("1.1.1.1:%d", port)), @@ -152,7 +152,7 @@ static CMutableTransaction CreateProUpServTx(const CChain& active_chain, const C static CMutableTransaction CreateProUpRegTx(const CChain& active_chain, const CTxMemPool& mempool, SimpleUTXOMap& utxos, const uint256& proTxHash, const CKey& mnKey, const CBLSPublicKey& pubKeyOperator, const CKeyID& keyIDVoting, const CScript& scriptPayout, const CKey& coinbaseKey) { CProUpRegTx proTx; - proTx.nVersion = ProTxVersion::GetMax(!bls::bls_legacy_scheme, /*is_extended_addr=*/false); + proTx.nVersion = ProTxVersion::GetMax(!bls::bls_legacy_scheme, /*is_extended_addr=*/false, /*is_multi_payout=*/false); proTx.proTxHash = proTxHash; proTx.pubKeyOperator.Set(pubKeyOperator, bls::bls_legacy_scheme.load()); proTx.keyIDVoting = keyIDVoting; @@ -173,7 +173,7 @@ static CMutableTransaction CreateProUpRegTx(const CChain& active_chain, const CT static CMutableTransaction CreateProUpRevTx(const CChain& active_chain, const CTxMemPool& mempool, SimpleUTXOMap& utxos, const uint256& proTxHash, const CBLSSecretKey& operatorKey, const CKey& coinbaseKey) { CProUpRevTx proTx; - proTx.nVersion = ProTxVersion::GetMax(!bls::bls_legacy_scheme, /*is_extended_addr=*/false); + proTx.nVersion = ProTxVersion::GetMax(!bls::bls_legacy_scheme, /*is_extended_addr=*/false, /*is_multi_payout=*/false); proTx.proTxHash = proTxHash; CMutableTransaction tx; @@ -641,7 +641,7 @@ void FuncTestMempoolReorg(TestChainSetup& setup) BOOST_CHECK_EQUAL(block->GetHash(), chainman.ActiveChain().Tip()->GetBlockHash()); CProRegTx payload; - payload.nVersion = ProTxVersion::GetMax(!bls::bls_legacy_scheme, /*is_extended_addr=*/false); + payload.nVersion = ProTxVersion::GetMax(!bls::bls_legacy_scheme, /*is_extended_addr=*/false, /*is_multi_payout=*/false); payload.netInfo = NetInfoInterface::MakeNetInfo(payload.nVersion); BOOST_CHECK_EQUAL(payload.netInfo->AddEntry(NetInfoPurpose::CORE_P2P, "1.1.1.1:1"), NetInfoStatus::Success); payload.keyIDOwner = ownerKey.GetPubKey().GetID(); @@ -717,7 +717,7 @@ void FuncTestMempoolDualProregtx(TestChainSetup& setup) auto scriptPayout = GetScriptForDestination(PKHash(payoutKey.GetPubKey())); CProRegTx payload; - payload.nVersion = ProTxVersion::GetMax(!bls::bls_legacy_scheme, /*is_extended_addr=*/false); + payload.nVersion = ProTxVersion::GetMax(!bls::bls_legacy_scheme, /*is_extended_addr=*/false, /*is_multi_payout=*/false); payload.netInfo = NetInfoInterface::MakeNetInfo(payload.nVersion); BOOST_CHECK_EQUAL(payload.netInfo->AddEntry(NetInfoPurpose::CORE_P2P, "1.1.1.1:2"), NetInfoStatus::Success); payload.keyIDOwner = ownerKey.GetPubKey().GetID(); @@ -787,7 +787,7 @@ void FuncVerifyDB(TestChainSetup& setup) BOOST_CHECK_EQUAL(block->GetHash(), chainman.ActiveChain().Tip()->GetBlockHash()); CProRegTx payload; - payload.nVersion = ProTxVersion::GetMax(!bls::bls_legacy_scheme, /*is_extended_addr=*/false); + payload.nVersion = ProTxVersion::GetMax(!bls::bls_legacy_scheme, /*is_extended_addr=*/false, /*is_multi_payout=*/false); payload.netInfo = NetInfoInterface::MakeNetInfo(payload.nVersion); BOOST_CHECK_EQUAL(payload.netInfo->AddEntry(NetInfoPurpose::CORE_P2P, "1.1.1.1:1"), NetInfoStatus::Success); payload.keyIDOwner = ownerKey.GetPubKey().GetID(); @@ -852,7 +852,7 @@ static CDeterministicMNCPtr create_mock_mn(uint64_t internal_id) dmnState->pubKeyOperator.Set(operatorKey.GetPublicKey(), bls::bls_legacy_scheme.load()); dmnState->keyIDVoting = ownerKey.GetPubKey().GetID(); dmnState->netInfo = NetInfoInterface::MakeNetInfo( - ProTxVersion::GetMax(!bls::bls_legacy_scheme, /*is_extended_addr=*/false)); + ProTxVersion::GetMax(!bls::bls_legacy_scheme, /*is_extended_addr=*/false, /*is_multi_payout=*/false)); BOOST_CHECK_EQUAL(dmnState->netInfo->AddEntry(NetInfoPurpose::CORE_P2P, "1.1.1.1:1"), NetInfoStatus::Success); auto dmn = std::make_shared(internal_id, MnType::Regular); diff --git a/src/test/evo_simplifiedmns_tests.cpp b/src/test/evo_simplifiedmns_tests.cpp index 1122ad6aef19..95770457e016 100644 --- a/src/test/evo_simplifiedmns_tests.cpp +++ b/src/test/evo_simplifiedmns_tests.cpp @@ -19,7 +19,7 @@ BOOST_AUTO_TEST_CASE(simplifiedmns_merkleroots) std::vector> entries; for (size_t i = 1; i < 16; i++) { CSimplifiedMNListEntry smle; - smle.nVersion = ProTxVersion::GetMax(!bls::bls_legacy_scheme, /*is_extended_addr=*/false); + smle.nVersion = ProTxVersion::GetMax(!bls::bls_legacy_scheme, /*is_extended_addr=*/false, /*is_multi_payout=*/false); smle.netInfo = NetInfoInterface::MakeNetInfo(smle.nVersion); smle.proRegTxHash.SetHex(strprintf("%064x", i)); smle.confirmedHash.SetHex(strprintf("%064x", i)); diff --git a/test/functional/rpc_blockchain.py b/test/functional/rpc_blockchain.py index 2f74a60003ba..81c4c04ffc8d 100755 --- a/test/functional/rpc_blockchain.py +++ b/test/functional/rpc_blockchain.py @@ -229,6 +229,17 @@ def _test_getblockchaininfo(self): 'ehf': True }, 'active': False}, + 'v25': { + 'type': 'bip9', + 'bip9': { + 'status': 'defined', + 'start_time': 0, + 'timeout': 9223372036854775807, # "v25" does not have a timeout so is set to the max int64 value + 'since': 0, + 'min_activation_height': 0, + 'ehf': True + }, + 'active': False}, 'testdummy': { 'type': 'bip9', 'bip9': { From fa889ac7b3c2f23bd4c72e8f853877678f8065c4 Mon Sep 17 00:00:00 2001 From: Hilawe Semunegus Date: Sat, 6 Jun 2026 01:16:03 -0400 Subject: [PATCH 02/15] DIP-0026 P2: PayoutShare data model + version-gated serialization Add the multi-party payout data model and wire its serialization into v4 ProRegTx/ProUpRegTx, with no validation or payout logic yet. - evo/providertx.h: new PayoutShare {CScript scriptPayout; uint16_t payoutShareReward;} with the DIP0026 wire format (CompactSize scriptlen + script bytes + uint16 reward); TOTAL_BASIS_POINTS=10000. Add std::vector payoutShares to CProRegTx and CProUpRegTx, serialized at the same wire position as scriptPayout but only for nVersion >= MultiPayout; pre-v4 serialization is byte-for-byte unchanged. Add GetPayoutShares() for a uniform view (synthesizes a single full share from scriptPayout for older versions). - evo/providertx.cpp: PayoutShare::ToString(). - test: new evo_providertx_tests (round-trip + wire-format + accessor coverage for PayoutShare, the share vector, and v2/v3/v4 ProReg/ProUpReg). Verified on macOS: 6 new tests pass; evo_dip3_activation_tests and the other evo serialization suites pass unchanged (no v<4 regression). --- src/Makefile.test.include | 1 + src/evo/providertx.cpp | 8 ++ src/evo/providertx.h | 80 ++++++++++++++-- src/test/evo_providertx_tests.cpp | 148 ++++++++++++++++++++++++++++++ 4 files changed, 229 insertions(+), 8 deletions(-) create mode 100644 src/test/evo_providertx_tests.cpp diff --git a/src/Makefile.test.include b/src/Makefile.test.include index 375c9940d059..d76789b37321 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -113,6 +113,7 @@ BITCOIN_TESTS =\ test/evo_islock_tests.cpp \ test/evo_mnhf_tests.cpp \ test/evo_netinfo_tests.cpp \ + test/evo_providertx_tests.cpp \ test/evo_simplifiedmns_tests.cpp \ test/evo_trivialvalidation.cpp \ test/evo_utils_tests.cpp \ diff --git a/src/evo/providertx.cpp b/src/evo/providertx.cpp index bf23da96b842..207285d651c8 100644 --- a/src/evo/providertx.cpp +++ b/src/evo/providertx.cpp @@ -53,6 +53,14 @@ template uint16_t GetMaxFromDeployment(gsl::not_null is_basic_override); } // namespace ProTxVersion +std::string PayoutShare::ToString() const +{ + CTxDestination dest; + const std::string payee{ExtractDestination(scriptPayout, dest) ? EncodeDestination(dest) + : HexStr(scriptPayout)}; + return strprintf("%s:%d", payee, payoutShareReward); +} + template bool IsNetInfoTriviallyValid(const ProTx& proTx, TxValidationState& state) { diff --git a/src/evo/providertx.h b/src/evo/providertx.h index 304908c0b324..fa2266adcd3d 100644 --- a/src/evo/providertx.h +++ b/src/evo/providertx.h @@ -77,6 +77,40 @@ template std::optional is_basic_override = std::nullopt); } // namespace ProTxVersion +/** + * DIP0026: a single multi-party payout share. The owner-side masternode block reward is split + * across a set of these. `payoutShareReward` is expressed in basis points (0..10000); the full + * set carried by a v4 ProRegTx/ProUpRegTx must sum to exactly TOTAL_BASIS_POINTS. + * + * Wire format (matches DIP0026): CompactSize scriptPayout length, scriptPayout bytes, uint16 + * payoutShareReward. A std::vector therefore serializes as a CompactSize count + * followed by that many encoded shares, which is exactly the DIP `payoutSharesSize` + + * `payoutShares[]` layout. + */ +class PayoutShare +{ +public: + static constexpr uint16_t TOTAL_BASIS_POINTS{10000}; + + CScript scriptPayout; + uint16_t payoutShareReward{0}; + + PayoutShare() = default; + PayoutShare(CScript script, uint16_t reward) : scriptPayout(std::move(script)), payoutShareReward(reward) {} + + SERIALIZE_METHODS(PayoutShare, obj) + { + READWRITE(obj.scriptPayout, obj.payoutShareReward); + } + + friend bool operator==(const PayoutShare& a, const PayoutShare& b) + { + return a.scriptPayout == b.scriptPayout && a.payoutShareReward == b.payoutShareReward; + } + + std::string ToString() const; +}; + class CProRegTx { public: @@ -94,7 +128,8 @@ class CProRegTx CBLSLazyPublicKey pubKeyOperator; CKeyID keyIDVoting; uint16_t nOperatorReward{0}; - CScript scriptPayout; + CScript scriptPayout; // used for nVersion < MultiPayout + std::vector payoutShares; // DIP0026, used for nVersion >= MultiPayout uint256 inputsHash; // replay protection std::vector vchSig; @@ -118,10 +153,16 @@ class CProRegTx obj.keyIDOwner, CBLSLazyPublicKeyVersionWrapper(const_cast(obj.pubKeyOperator), (obj.nVersion == ProTxVersion::LegacyBLS)), obj.keyIDVoting, - obj.nOperatorReward, - obj.scriptPayout, - obj.inputsHash + obj.nOperatorReward ); + // DIP0026: v4+ replaces the single scriptPayout with an array of payout shares at the + // same wire position. Pre-v4 serialization is byte-for-byte unchanged. + if (obj.nVersion < ProTxVersion::MultiPayout) { + READWRITE(obj.scriptPayout); + } else { + READWRITE(obj.payoutShares); + } + READWRITE(obj.inputsHash); if (obj.nType == MnType::Evo) { READWRITE( obj.platformNodeID); @@ -136,6 +177,15 @@ class CProRegTx } } + // Uniform view of the owner-side payout regardless of version: for nVersion >= MultiPayout + // returns the stored shares; for older versions synthesizes a single full share from + // scriptPayout. Lets downstream payout/validation code have a single code path. + [[nodiscard]] std::vector GetPayoutShares() const + { + if (nVersion >= ProTxVersion::MultiPayout) return payoutShares; + return {PayoutShare{scriptPayout, PayoutShare::TOTAL_BASIS_POINTS}}; + } + // When signing with the collateral key, we don't sign the hash but a generated message instead // This is needed for HW wallet support which can only sign text messages as of now std::string MakeSignString() const; @@ -221,7 +271,8 @@ class CProUpRegTx uint16_t nMode{0}; // only 0 supported for now CBLSLazyPublicKey pubKeyOperator; CKeyID keyIDVoting; - CScript scriptPayout; + CScript scriptPayout; // used for nVersion < MultiPayout + std::vector payoutShares; // DIP0026, used for nVersion >= MultiPayout uint256 inputsHash; // replay protection std::vector vchSig; @@ -239,10 +290,16 @@ class CProUpRegTx obj.proTxHash, obj.nMode, CBLSLazyPublicKeyVersionWrapper(const_cast(obj.pubKeyOperator), (obj.nVersion == ProTxVersion::LegacyBLS)), - obj.keyIDVoting, - obj.scriptPayout, - obj.inputsHash + obj.keyIDVoting ); + // DIP0026: v4+ replaces the single scriptPayout with an array of payout shares at the + // same wire position. Pre-v4 serialization is byte-for-byte unchanged. + if (obj.nVersion < ProTxVersion::MultiPayout) { + READWRITE(obj.scriptPayout); + } else { + READWRITE(obj.payoutShares); + } + READWRITE(obj.inputsHash); if (!(s.GetType() & SER_GETHASH)) { READWRITE( obj.vchSig @@ -250,6 +307,13 @@ class CProUpRegTx } } + // Uniform view of the owner-side payout regardless of version (see CProRegTx::GetPayoutShares). + [[nodiscard]] std::vector GetPayoutShares() const + { + if (nVersion >= ProTxVersion::MultiPayout) return payoutShares; + return {PayoutShare{scriptPayout, PayoutShare::TOTAL_BASIS_POINTS}}; + } + std::string ToString() const; [[nodiscard]] static RPCResult GetJsonHelp(const std::string& key, bool optional); diff --git a/src/test/evo_providertx_tests.cpp b/src/test/evo_providertx_tests.cpp new file mode 100644 index 000000000000..89d754437927 --- /dev/null +++ b/src/test/evo_providertx_tests.cpp @@ -0,0 +1,148 @@ +// Copyright (c) 2025 The Dash Core developers +// 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