diff --git a/doc/release-notes-7807.md b/doc/release-notes-7807.md new file mode 100644 index 000000000000..ce06a893ef4b --- /dev/null +++ b/doc/release-notes-7807.md @@ -0,0 +1,20 @@ +Consensus (v24, not yet activated on mainnet or testnet) +--------------------------------------------------------- + +- EvoNodes cannot register with shared collateral or have more than one owner payout until + the new `evo_shares` deployment activates. An EvoNode ProRegTx with a share table is + rejected with `bad-protx-shares-evo`, and an EvoNode ProRegTx or ProUpRegTx with more than + one owner payout is rejected with `bad-protx-payouts-evo`. Regular masternodes are not + affected. +- `evo_shares` (version bit 14) is a masternode-activated deployment with the same parameters + as `v24`. This release follows it but its masternodes do not sign it, so it stays inactive + until a later release signs it once Platform supports EvoNode shares and payouts. +- The consent digest that share owners sign for a shared EvoNode registration also covers the + EvoNode's `platformNodeID`. The digest of a regular shared registration is unchanged. + +New RPCs +-------- + +- `protx shared_register_prepare_evo` creates an unsigned shared EvoNode registration. It + takes the arguments of `protx shared_register_prepare` followed by `platformNodeID`, + `platformP2PAddrs` and `platformHTTPSAddrs`, and fails until `evo_shares` is active. diff --git a/src/chainparams.cpp b/src/chainparams.cpp index 901491876a69..c46bc0a738b8 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_EVO_SHARES].bit = 14; + consensus.vDeployments[Consensus::DEPLOYMENT_EVO_SHARES].nStartTime = Consensus::BIP9Deployment::NEVER_ACTIVE; // TODO: same as DEPLOYMENT_V24 + consensus.vDeployments[Consensus::DEPLOYMENT_EVO_SHARES].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; // TODO: same as DEPLOYMENT_V24 + consensus.vDeployments[Consensus::DEPLOYMENT_EVO_SHARES].nWindowSize = 4032; + consensus.vDeployments[Consensus::DEPLOYMENT_EVO_SHARES].nThresholdStart = 3226; // 80% of 4032 + consensus.vDeployments[Consensus::DEPLOYMENT_EVO_SHARES].nThresholdMin = 2420; // 60% of 4032 + consensus.vDeployments[Consensus::DEPLOYMENT_EVO_SHARES].nFalloffCoeff = 5; // this corresponds to 10 periods + consensus.vDeployments[Consensus::DEPLOYMENT_EVO_SHARES].useEHF = true; + consensus.nMinimumChainWork = uint256S("0x00000000000000000000000000000000000000000000ba7225adc3153945d14d"); // 2487500 consensus.defaultAssumeValid = uint256S("0x00000000000000119fe42827219e0686d3f7b494ae65f823194c740c5dbab492"); // 2487500 @@ -418,6 +427,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_EVO_SHARES].bit = 14; + consensus.vDeployments[Consensus::DEPLOYMENT_EVO_SHARES].nStartTime = 1790208000; // Thursday, September 24, 2026 0:00:00 + consensus.vDeployments[Consensus::DEPLOYMENT_EVO_SHARES].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; + consensus.vDeployments[Consensus::DEPLOYMENT_EVO_SHARES].nWindowSize = 100; + consensus.vDeployments[Consensus::DEPLOYMENT_EVO_SHARES].nThresholdStart = 80; // 80% of 100 + consensus.vDeployments[Consensus::DEPLOYMENT_EVO_SHARES].nThresholdMin = 60; // 60% of 100 + consensus.vDeployments[Consensus::DEPLOYMENT_EVO_SHARES].nFalloffCoeff = 5; // this corresponds to 10 periods + consensus.vDeployments[Consensus::DEPLOYMENT_EVO_SHARES].useEHF = true; + consensus.nMinimumChainWork = uint256S("0x000000000000000000000000000000000000000000000000036c8f738da818d2"); // 1400000 consensus.defaultAssumeValid = uint256S("0x000000541a23f9db7411cddbe50f9f1ebd4aa7108ebdcad62214753f648c0239"); // 1400000 @@ -591,6 +609,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_EVO_SHARES].bit = 14; + consensus.vDeployments[Consensus::DEPLOYMENT_EVO_SHARES].nStartTime = 1751328000; // July 1, 2025 + consensus.vDeployments[Consensus::DEPLOYMENT_EVO_SHARES].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; + consensus.vDeployments[Consensus::DEPLOYMENT_EVO_SHARES].nWindowSize = 120; + consensus.vDeployments[Consensus::DEPLOYMENT_EVO_SHARES].nThresholdStart = 96; // 80% of 120 + consensus.vDeployments[Consensus::DEPLOYMENT_EVO_SHARES].nThresholdMin = 72; // 60% of 120 + consensus.vDeployments[Consensus::DEPLOYMENT_EVO_SHARES].nFalloffCoeff = 5; // this corresponds to 10 periods + consensus.vDeployments[Consensus::DEPLOYMENT_EVO_SHARES].useEHF = true; + consensus.nMinimumChainWork = uint256{}; consensus.defaultAssumeValid = uint256{}; @@ -826,6 +853,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_EVO_SHARES].bit = 14; + consensus.vDeployments[Consensus::DEPLOYMENT_EVO_SHARES].nStartTime = 0; + consensus.vDeployments[Consensus::DEPLOYMENT_EVO_SHARES].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; + consensus.vDeployments[Consensus::DEPLOYMENT_EVO_SHARES].nWindowSize = 250; + consensus.vDeployments[Consensus::DEPLOYMENT_EVO_SHARES].nThresholdStart = 250 / 5 * 4; // 80% of window size + consensus.vDeployments[Consensus::DEPLOYMENT_EVO_SHARES].nThresholdMin = 250 / 5 * 3; // 60% of window size + consensus.vDeployments[Consensus::DEPLOYMENT_EVO_SHARES].nFalloffCoeff = 5; // this corresponds to 10 periods + consensus.vDeployments[Consensus::DEPLOYMENT_EVO_SHARES].useEHF = true; + consensus.nMinimumChainWork = uint256{}; consensus.defaultAssumeValid = uint256{}; diff --git a/src/consensus/params.h b/src/consensus/params.h index 20c20531abcb..7a6f724b8e48 100644 --- a/src/consensus/params.h +++ b/src/consensus/params.h @@ -42,6 +42,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_EVO_SHARES, // Deployment of EvoNode shared collateral and multiple owner 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..594bf6615a08 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 =*/"evo_shares", + /*.gbt_force =*/true, + }, }; std::string DeploymentName(Consensus::BuriedDeployment dep) diff --git a/src/evo/providertx.cpp b/src/evo/providertx.cpp index 4037fa8bf302..c10b312a441a 100644 --- a/src/evo/providertx.cpp +++ b/src/evo/providertx.cpp @@ -278,9 +278,6 @@ bool CProRegTx::IsTriviallyValid(TxValidationState& state) const } if (IsShared()) { - if (nType != MnType::Regular) { - return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-shares-evo"); - } // The collateral must be internal; the funding inputs and outputs are covered by the consent digest if (!collateralOutpoint.hash.IsNull()) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-shares-external"); @@ -359,6 +356,9 @@ uint256 CProRegTx::MakeSharedRegConsentHash(const CTransaction& tx) const hw << nMode; hw << NetInfoSerWrapper(const_cast&>(netInfo), nVersion >= ProTxVersion::ExtAddr); + if (nType == MnType::Evo) { + hw << platformNodeID; + } hw << keyIDVoting; hw << CBLSLazyPublicKeyVersionWrapper(const_cast(pubKeyOperator), nVersion == ProTxVersion::LegacyBLS); diff --git a/src/evo/providertx_service.cpp b/src/evo/providertx_service.cpp index 3d3f6ed5f439..aa9426567183 100644 --- a/src/evo/providertx_service.cpp +++ b/src/evo/providertx_service.cpp @@ -99,6 +99,14 @@ uint16_t CurrentProviderTxVersion(node::NodeContext& node, std::optional b return DeploymentToProtxVersion(chainman.ActiveChain().Tip(), chainman, basic_override); } +bool EvoSharesActive(node::NodeContext& node) +{ + AssertLockNotHeld(::cs_main); + auto& chainman{*Assert(node.chainman)}; + LOCK(::cs_main); + return DeploymentActiveAfter(chainman.ActiveChain().Tip(), chainman, Consensus::DEPLOYMENT_EVO_SHARES); +} + struct ChainSnapshot { uint16_t provider_tx_version{0}; CDeterministicMNCPtr dmn; @@ -255,6 +263,17 @@ std::optional ValidateNetworkFields(const ProTx& payload, bool return std::nullopt; } +template +std::optional ApplyPlatformNodeId(ProTx& payload, const std::optional& platform_node_id) +{ + if (payload.nType != MnType::Evo) return std::nullopt; + if (!platform_node_id) { + return Error(ProviderTxErrorCode::INVALID_PARAMETER, "platformNodeID must be specified for an EvoNode"); + } + payload.platformNodeID = *platform_node_id; + return std::nullopt; +} + using PayoutResult = std::variant; PayoutResult BuildPayouts(const std::vector& payouts) @@ -360,9 +379,8 @@ std::optional Preflight(node::NodeContext& node, const CTransac } TxValidationState state; - const bool is_v24_active{DeploymentActiveAfter(tip, chainman, Consensus::DEPLOYMENT_V24)}; - if (!chain_helper.special_tx->CheckSpecialTx(tx, tip, is_v24_active, chainman.ActiveChainstate().CoinsTip(), true, - state)) { + if (!chain_helper.special_tx->CheckSpecialTx(tx, tip, GetSpecialTxRules(tip, chainman), + chainman.ActiveChainstate().CoinsTip(), true, state)) { return Error(ProviderTxErrorCode::CONSENSUS_REJECTED, state.ToString(), state.GetRejectReason()); } return std::nullopt; @@ -515,12 +533,7 @@ RegistrationResult BuildRegistration(node::NodeContext& node, Wallet& wallet, return Error(ProviderTxErrorCode::INVALID_PARAMETER, "payouts array requires provider transaction version 3"); } - if (request.type == MnType::Evo) { - if (!request.platform_node_id) { - return Error(ProviderTxErrorCode::INVALID_PARAMETER, "platformNodeID must be specified for an EvoNode"); - } - payload.platformNodeID = *request.platform_node_id; - } + if (auto error{ApplyPlatformNodeId(payload, request.platform_node_id)}) return *error; if (auto error{ValidateNetworkFields(payload, /*allow_empty=*/true, /*check_platform_node_id=*/true)}) { return *error; @@ -822,12 +835,7 @@ ProviderTxResult UpdateService(node::NodeContext& node, Wa if (auto error{ApplyNetInfo(payload, request.net_info, request.type == MnType::Evo, /*optional=*/false)}) { return *error; } - if (request.type == MnType::Evo) { - if (!request.platform_node_id) { - return Error(ProviderTxErrorCode::INVALID_PARAMETER, "platformNodeID must be specified for an EvoNode"); - } - payload.platformNodeID = *request.platform_node_id; - } + if (auto error{ApplyPlatformNodeId(payload, request.platform_node_id)}) return *error; if (auto error{ValidateNetworkFields(payload, /*allow_empty=*/false, /*check_platform_node_id=*/true)}) { return *error; @@ -992,12 +1000,20 @@ ProviderTxResult Revoke(node::NodeContext& node, Wallet& w ProviderTxResult PrepareSharedRegistration(node::NodeContext& node, const SharedRegistrationRequest& request) { + if (!IsValidMnType(request.type)) { + return Error(ProviderTxErrorCode::INVALID_PARAMETER, "invalid masternode type"); + } + if (request.type == MnType::Evo && !EvoSharesActive(node)) { + return Error(ProviderTxErrorCode::INVALID_PARAMETER, + "EvoNode shared registration requires the evo_shares deployment to be active"); + } + CMutableTransaction tx{request.funding_tx}; tx.nVersion = 3; tx.nType = TRANSACTION_PROVIDER_REGISTER; CProRegTx payload; - payload.nType = MnType::Regular; + payload.nType = request.type; payload.nVersion = CurrentProviderTxVersion(node); if (payload.nVersion < ProTxVersion::ExtAddr) { return Error(ProviderTxErrorCode::INVALID_PARAMETER, @@ -1023,7 +1039,13 @@ ProviderTxResult PrepareSharedRegistration(node::Nod share.reward ? GetScriptForDestination(*share.reward) : CScript{}, share.owner); } - if (auto error{ApplyNetInfo(payload, request.net_info, /*platform=*/false, /*optional=*/true)}) return *error; + if (auto error{ApplyNetInfo(payload, request.net_info, /*platform=*/payload.nType == MnType::Evo, /*optional=*/true)}) { + return *error; + } + if (auto error{ApplyPlatformNodeId(payload, request.platform_node_id)}) return *error; + if (auto error{ValidateNetworkFields(payload, /*allow_empty=*/true, /*check_platform_node_id=*/true)}) { + return *error; + } if (!request.operator_key.IsValid()) { return Error(ProviderTxErrorCode::INVALID_PARAMETER, "operator BLS address must be a valid BLS public key"); diff --git a/src/evo/specialtxman.cpp b/src/evo/specialtxman.cpp index be1f0f18175c..28f59ec6ced5 100644 --- a/src/evo/specialtxman.cpp +++ b/src/evo/specialtxman.cpp @@ -215,7 +215,7 @@ bool CheckCbTxBestChainlock(const CCbTx& cbTx, const CBlockIndex* pindex, const } bool CSpecialTxProcessor::CheckSpecialTxInner(const CChain* chain, const CTransaction& tx, - const CBlockIndex* pindexPrev, bool is_v24_active, + const CBlockIndex* pindexPrev, SpecialTxRules rules, const CCoinsViewCache& view, const std::optional& indexes, bool check_sigs, TxValidationState& state) { @@ -231,26 +231,25 @@ bool CSpecialTxProcessor::CheckSpecialTxInner(const CChain* chain, const CTransa try { switch (tx.nType) { case TRANSACTION_PROVIDER_REGISTER: - return CheckProRegTx(tx, pindexPrev, m_dmnman, view, m_consensus_params, is_v24_active, state, check_sigs); + return CheckProRegTx(tx, pindexPrev, m_dmnman, view, m_consensus_params, rules, state, check_sigs); case TRANSACTION_PROVIDER_UPDATE_SERVICE: - return CheckProUpServTx(tx, pindexPrev, m_dmnman, m_consensus_params, is_v24_active, state, check_sigs); + return CheckProUpServTx(tx, pindexPrev, m_dmnman, m_consensus_params, rules.v24, state, check_sigs); case TRANSACTION_PROVIDER_UPDATE_REGISTRAR: - return CheckProUpRegTx(tx, pindexPrev, m_dmnman, view, m_consensus_params, is_v24_active, state, check_sigs); + return CheckProUpRegTx(tx, pindexPrev, m_dmnman, view, m_consensus_params, rules, state, check_sigs); case TRANSACTION_PROVIDER_UPDATE_REVOKE: - return CheckProUpRevTx(tx, pindexPrev, m_dmnman, m_consensus_params, is_v24_active, state, check_sigs); + return CheckProUpRevTx(tx, pindexPrev, m_dmnman, m_consensus_params, rules.v24, state, check_sigs); case TRANSACTION_PROVIDER_DISSOLVE: - return CheckProDisTx(tx, pindexPrev, m_dmnman, m_consensus_params, is_v24_active, state, check_sigs); + return CheckProDisTx(tx, pindexPrev, m_dmnman, m_consensus_params, rules.v24, state, check_sigs); case TRANSACTION_PROVIDER_UPDATE_SHARE: - return CheckProUpShareTx(tx, pindexPrev, m_dmnman, m_consensus_params, is_v24_active, state, check_sigs); + return CheckProUpShareTx(tx, pindexPrev, m_dmnman, m_consensus_params, rules.v24, state, check_sigs); case TRANSACTION_PROVIDER_UPDATE_SHARED_REGISTRAR: - return CheckProUpSharedRegTx(tx, pindexPrev, m_dmnman, m_consensus_params, is_v24_active, state, - check_sigs); + return CheckProUpSharedRegTx(tx, pindexPrev, m_dmnman, m_consensus_params, rules.v24, state, check_sigs); case TRANSACTION_COINBASE: { if (!tx.IsCoinBase()) { return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-cbtx-invalid"); } if (const auto opt_cbTx = GetTxPayload(tx)) { - return CheckCbTx(*opt_cbTx, pindexPrev, is_v24_active, state); + return CheckCbTx(*opt_cbTx, pindexPrev, rules.v24, state); } else { return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-cbtx-payload"); } @@ -261,10 +260,10 @@ bool CSpecialTxProcessor::CheckSpecialTxInner(const CChain* chain, const CTransa return chain ? CheckMNHFTx(m_blockman, m_qman, *chain, tx, pindexPrev, state) : CheckMNHFTx(m_blockman, m_qman, tx, pindexPrev, state); case TRANSACTION_ASSET_LOCK: - return CheckAssetLockTx(tx, state, is_v24_active); + return CheckAssetLockTx(tx, state, rules.v24); case TRANSACTION_ASSET_UNLOCK: - return chain ? CheckAssetUnlockTx(m_blockman, m_qman, *chain, tx, pindexPrev, indexes, is_v24_active, state) : - CheckAssetUnlockTx(m_blockman, m_qman, tx, pindexPrev, indexes, is_v24_active, state); + return chain ? CheckAssetUnlockTx(m_blockman, m_qman, *chain, tx, pindexPrev, indexes, rules.v24, state) + : CheckAssetUnlockTx(m_blockman, m_qman, tx, pindexPrev, indexes, rules.v24, state); } } catch (const std::exception& e) { LogPrintf("%s -- failed: %s\n", __func__, e.what()); @@ -274,11 +273,11 @@ bool CSpecialTxProcessor::CheckSpecialTxInner(const CChain* chain, const CTransa return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-tx-type-check"); } -bool CSpecialTxProcessor::CheckSpecialTx(const CTransaction& tx, const CBlockIndex* pindexPrev, bool is_v24_active, +bool CSpecialTxProcessor::CheckSpecialTx(const CTransaction& tx, const CBlockIndex* pindexPrev, SpecialTxRules rules, const CCoinsViewCache& view, bool check_sigs, TxValidationState& state) { AssertLockHeld(::cs_main); - return CheckSpecialTxInner(nullptr, tx, pindexPrev, is_v24_active, view, std::nullopt, check_sigs, state); + return CheckSpecialTxInner(nullptr, tx, pindexPrev, rules, view, std::nullopt, check_sigs, state); } static void HandleQuorumCommitment(const llmq::CFinalCommitment& qc, const std::vector& members, @@ -796,7 +795,7 @@ bool CSpecialTxProcessor::RebuildListFromBlock(const CBlock& block, gsl::not_nul } bool CSpecialTxProcessor::ProcessSpecialTxsInBlock(Chainstate& chainstate, const CChain& chain, const CBlock& block, - const CBlockIndex* pindex, bool is_v24_active, + const CBlockIndex* pindex, SpecialTxRules rules, const CCoinsViewCache& view, CAmount blockSubsidy, bool fJustCheck, bool fCheckCbTxMerkleRoots, BlockValidationState& state, MNListUpdates& updatesRet) @@ -821,7 +820,7 @@ bool CSpecialTxProcessor::ProcessSpecialTxsInBlock(Chainstate& chainstate, const } if (opt_cbTx = GetTxPayload(*tx); opt_cbTx) { TxValidationState tx_state; - if (!CheckCbTx(*opt_cbTx, pindex->pprev, is_v24_active, tx_state)) { + if (!CheckCbTx(*opt_cbTx, pindex->pprev, rules.v24, tx_state)) { assert(tx_state.GetResult() == TxValidationResult::TX_CONSENSUS || tx_state.GetResult() == TxValidationResult::TX_BAD_SPECIAL); return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, tx_state.GetRejectReason(), @@ -854,7 +853,7 @@ bool CSpecialTxProcessor::ProcessSpecialTxsInBlock(Chainstate& chainstate, const for (size_t i = 0; i < block.vtx.size(); ++i) { // The shared-collateral creation rule applies to every transaction, including the // coinbase and non-special transactions, which CheckSpecialTxInner never sees - if (is_v24_active) { + if (rules.v24) { if (TxValidationState tx_state; !CheckSharedCollateralTemplateOutputs(*block.vtx[i], tx_state)) { return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, tx_state.GetRejectReason(), strprintf("Shared collateral check failed (tx hash %s) %s", @@ -869,8 +868,7 @@ bool CSpecialTxProcessor::ProcessSpecialTxsInBlock(Chainstate& chainstate, const TxValidationState tx_state; // At this moment CheckSpecialTx() may fail by 2 possible ways: // consensus failures and "TX_BAD_SPECIAL" - if (!CheckSpecialTxInner(&chain, *ptr_tx, pindex->pprev, is_v24_active, view, indexes, fCheckCbTxMerkleRoots, - tx_state)) { + if (!CheckSpecialTxInner(&chain, *ptr_tx, pindex->pprev, rules, view, indexes, fCheckCbTxMerkleRoots, tx_state)) { assert(tx_state.GetResult() == TxValidationResult::TX_CONSENSUS || tx_state.GetResult() == TxValidationResult::TX_BAD_SPECIAL); return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, tx_state.GetRejectReason(), strprintf("Special Transaction check failed (tx hash %s) %s", ptr_tx->GetHash().ToString(), tx_state.GetDebugMessage())); @@ -906,7 +904,7 @@ bool CSpecialTxProcessor::ProcessSpecialTxsInBlock(Chainstate& chainstate, const CDeterministicMNList mn_list; if (DeploymentActiveAt(*pindex, m_consensus_params, Consensus::DEPLOYMENT_DIP0003)) { - if (!BuildNewListFromBlock(block, pindex->pprev, is_v24_active, view, true, state, mn_list)) { + if (!BuildNewListFromBlock(block, pindex->pprev, rules.v24, view, true, state, mn_list)) { // pass the state returned by the function above return false; } @@ -1148,20 +1146,29 @@ static bool IsVersionChangeValid(const uint16_t state_version, const uint16_t tx } bool CheckProRegTx(const CTransaction& tx, gsl::not_null pindexPrev, CDeterministicMNManager& dmnman, - const CCoinsViewCache& view, const Consensus::Params& consensus_params, bool is_v24_active, + const CCoinsViewCache& view, const Consensus::Params& consensus_params, SpecialTxRules rules, TxValidationState& state, bool check_sigs) { - const auto opt_ptx = GetValidatedPayload(tx, pindexPrev, consensus_params, is_v24_active, state); + const auto opt_ptx = GetValidatedPayload(tx, pindexPrev, consensus_params, rules.v24, state); if (!opt_ptx) { // pass the state returned by the function above return false; } // No longer allow legacy scheme masternode registration - if (is_v24_active && opt_ptx->nVersion < ProTxVersion::BasicBLS) { + if (rules.v24 && opt_ptx->nVersion < ProTxVersion::BasicBLS) { return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-protx-version-disallowed"); } + if (opt_ptx->nType == MnType::Evo && !rules.evo_shares) { + if (opt_ptx->IsShared()) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-shares-evo"); + } + if (GetOwnerPayouts(*opt_ptx).size() > 1) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-payouts-evo"); + } + } + // It's allowed to set addr to 0, which will put the MN into PoSe-banned state and require a ProUpServTx to be // issues later. If any of both is set, it must be valid however if (!CheckProviderNetworkFields(opt_ptx->netInfo, opt_ptx->nType, opt_ptx->nVersion, &opt_ptx->platformNodeID, @@ -1262,7 +1269,7 @@ bool CheckProRegTx(const CTransaction& tx, gsl::not_null pin // proves ownership of the operator key, so that gap lets anyone claim a masternode's key. // Nothing is excluded here: a duplicate key is never allowed, even for a ProTx replacing an // existing masternode. - if (is_v24_active && mnList.HasOperatorKeyUnderAnyScheme(opt_ptx->pubKeyOperator.Get(), /*self=*/uint256())) { + if (rules.v24 && mnList.HasOperatorKeyUnderAnyScheme(opt_ptx->pubKeyOperator.Get(), /*self=*/uint256())) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-dup-key"); } @@ -1419,10 +1426,10 @@ bool CheckProUpServTx(const CTransaction& tx, gsl::not_null } bool CheckProUpRegTx(const CTransaction& tx, gsl::not_null pindexPrev, CDeterministicMNManager& dmnman, - const CCoinsViewCache& view, const Consensus::Params& consensus_params, bool is_v24_active, + const CCoinsViewCache& view, const Consensus::Params& consensus_params, SpecialTxRules rules, TxValidationState& state, bool check_sigs) { - const auto opt_ptx = GetValidatedPayload(tx, pindexPrev, consensus_params, is_v24_active, state); + const auto opt_ptx = GetValidatedPayload(tx, pindexPrev, consensus_params, rules.v24, state); if (!opt_ptx) { // pass the state returned by the function above return false; @@ -1440,7 +1447,7 @@ bool CheckProUpRegTx(const CTransaction& tx, gsl::not_null p return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-shared-mn"); } - if (!IsVersionChangeValid(dmn->pdmnState->nVersion, opt_ptx->nVersion, is_v24_active, state)) { + if (!IsVersionChangeValid(dmn->pdmnState->nVersion, opt_ptx->nVersion, rules.v24, state)) { // pass the state returned by the function above return false; } @@ -1450,7 +1457,7 @@ bool CheckProUpRegTx(const CTransaction& tx, gsl::not_null p // if that target slot is already held by another masternode -- under either encoding -- so the // re-key in UpdateMN() cannot collide and throw out of block assembly. Scoped to those two cases // so a pre-existing cross-scheme pair's non-migrating routine update is not blocked. - if (is_v24_active) { + if (rules.v24) { const bool key_changed{!(opt_ptx->pubKeyOperator == dmn->pdmnState->pubKeyOperator)}; const bool migrating{IsSchemeMigration(dmn->pdmnState->nVersion, opt_ptx->nVersion)}; if ((key_changed || migrating) && @@ -1473,6 +1480,9 @@ bool CheckProUpRegTx(const CTransaction& tx, gsl::not_null p const auto owner_payouts = GetOwnerPayouts(*opt_ptx); if (!IsPayoutListTriviallyValid(owner_payouts, dmn->pdmnState->keyIDOwner, opt_ptx->keyIDVoting, state)) return false; + if (dmn->nType == MnType::Evo && !rules.evo_shares && owner_payouts.size() > 1) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-payouts-evo"); + } Coin coin; if (!view.GetCoin(dmn->collateralOutpoint, coin) || coin.IsSpent()) { diff --git a/src/evo/specialtxman.h b/src/evo/specialtxman.h index cb900c749d89..c90858d22062 100644 --- a/src/evo/specialtxman.h +++ b/src/evo/specialtxman.h @@ -44,6 +44,14 @@ namespace node { class BlockManager; } // namespace node +/** Activation state of the deployments that special transaction validation depends on, evaluated by the + * caller for the block after pindexPrev (see GetSpecialTxRules() in validation.h) */ +struct SpecialTxRules { + bool v24{false}; + //! EvoNodes may register with shared collateral and have multiple owner payouts + bool evo_shares{false}; +}; + class CSpecialTxProcessor { private: @@ -77,13 +85,14 @@ class CSpecialTxProcessor { } - bool CheckSpecialTx(const CTransaction& tx, const CBlockIndex* pindexPrev, bool is_v24_active, + bool CheckSpecialTx(const CTransaction& tx, const CBlockIndex* pindexPrev, SpecialTxRules rules, const CCoinsViewCache& view, bool check_sigs, TxValidationState& state) EXCLUSIVE_LOCKS_REQUIRED(::cs_main); - bool ProcessSpecialTxsInBlock(Chainstate& chainstate, const CChain& chain, const CBlock& block, const CBlockIndex* pindex, - bool is_v24_active, const CCoinsViewCache& view, CAmount blockSubsidy, bool fJustCheck, - bool fCheckCbTxMerkleRoots, BlockValidationState& state, - MNListUpdates& updatesRet) EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + bool ProcessSpecialTxsInBlock(Chainstate& chainstate, const CChain& chain, const CBlock& block, + const CBlockIndex* pindex, SpecialTxRules rules, const CCoinsViewCache& view, + CAmount blockSubsidy, bool fJustCheck, bool fCheckCbTxMerkleRoots, + BlockValidationState& state, MNListUpdates& updatesRet) + EXCLUSIVE_LOCKS_REQUIRED(::cs_main); bool UndoSpecialTxsInBlock(const Chainstate& chainstate, const CBlock& block, const CBlockIndex* pindex, MNListUpdates& updatesRet) EXCLUSIVE_LOCKS_REQUIRED(::cs_main); @@ -101,7 +110,7 @@ class CSpecialTxProcessor private: bool CheckSpecialTxInner(const CChain* chain, const CTransaction& tx, const CBlockIndex* pindexPrev, - bool is_v24_active, const CCoinsViewCache& view, const std::optional& indexes, + SpecialTxRules rules, const CCoinsViewCache& view, const std::optional& indexes, bool check_sigs, TxValidationState& state) EXCLUSIVE_LOCKS_REQUIRED(::cs_main); bool CheckCreditPoolDiffForBlock(const CBlock& block, const CBlockIndex* pindex, const CCbTx& cbTx, CAmount blockSubsidy, BlockValidationState& state) EXCLUSIVE_LOCKS_REQUIRED(::cs_main); @@ -123,13 +132,13 @@ bool CheckCbTxBestChainlock(const CCbTx& cbTx, const CBlockIndex* pindex, const const chainlock::Chainlocks& chainlocks, BlockValidationState& state); bool CheckProRegTx(const CTransaction& tx, gsl::not_null pindexPrev, CDeterministicMNManager& dmnman, - const CCoinsViewCache& view, const Consensus::Params& consensus_params, bool is_v24_active, + const CCoinsViewCache& view, const Consensus::Params& consensus_params, SpecialTxRules rules, TxValidationState& state, bool check_sigs); bool CheckProUpServTx(const CTransaction& tx, gsl::not_null pindexPrev, CDeterministicMNManager& dmnman, const Consensus::Params& consensus_params, bool is_v24_active, TxValidationState& state, bool check_sigs); bool CheckProUpRegTx(const CTransaction& tx, gsl::not_null pindexPrev, CDeterministicMNManager& dmnman, - const CCoinsViewCache& view, const Consensus::Params& consensus_params, bool is_v24_active, + const CCoinsViewCache& view, const Consensus::Params& consensus_params, SpecialTxRules rules, TxValidationState& state, bool check_sigs); bool CheckProUpRevTx(const CTransaction& tx, gsl::not_null pindexPrev, CDeterministicMNManager& dmnman, const Consensus::Params& consensus_params, bool is_v24_active, diff --git a/src/instantsend/signing.cpp b/src/instantsend/signing.cpp index 921b7291014e..bf5749a174a3 100644 --- a/src/instantsend/signing.cpp +++ b/src/instantsend/signing.cpp @@ -211,8 +211,8 @@ bool InstantSendSigner::CheckCanLockAssetUnlock(const CTransaction& tx, bool pri const CBlockIndex* tip = chainstate.m_chain.Tip(); // Minable in the next block: inside its height window and signed by a recent quorum TxValidationState state; - const bool is_v24_active{DeploymentActiveAfter(tip, m_chainman, Consensus::DEPLOYMENT_V24)}; - if (!chainstate.ChainHelper().special_tx->CheckSpecialTx(tx, tip, is_v24_active, chainstate.CoinsTip(), + if (!chainstate.ChainHelper().special_tx->CheckSpecialTx(tx, tip, GetSpecialTxRules(tip, m_chainman), + chainstate.CoinsTip(), /*check_sigs=*/true, state)) { return log_refusal(state.ToString()); } diff --git a/src/interfaces/providertx.h b/src/interfaces/providertx.h index 63decede3568..d23c71dee4c2 100644 --- a/src/interfaces/providertx.h +++ b/src/interfaces/providertx.h @@ -153,9 +153,12 @@ struct SharedShareSpec { //! Turns a funding transaction that already holds every participant's inputs and change //! into an unsigned shared ProRegTx. Needs no wallet: nothing is funded or signed. struct SharedRegistrationRequest { + MnType type{MnType::Regular}; CMutableTransaction funding_tx; std::vector shares; ProviderNetInfo net_info; + //! Required for an EvoNode + std::optional platform_node_id; CBLSPublicKey operator_key; CKeyID voting_key; uint16_t operator_reward{0}; diff --git a/src/llmq/ehf_signals.cpp b/src/llmq/ehf_signals.cpp index 6f0077455c98..fc8563fdc197 100644 --- a/src/llmq/ehf_signals.cpp +++ b/src/llmq/ehf_signals.cpp @@ -45,9 +45,14 @@ void CEHFSignalsHandler::UpdatedBlockTip(const CBlockIndex* const pindexNew) const auto ehfSignals = m_chainman.ActiveChainstate().ChainHelper().ehf_manager->GetSignalsStage(pindexNew); const int64_t time_past = pindexNew->GetMedianTimePast(); - for (const auto& deployment : Params().GetConsensus().vDeployments) { + for (int i = 0; i < Consensus::MAX_VERSION_BITS_DEPLOYMENTS; ++i) { + const auto pos{static_cast(i)}; + const auto& deployment{Params().GetConsensus().vDeployments[pos]}; // Skip deployments that do not use dip0023 if (!deployment.useEHF) continue; + // v24 follows evo_shares without signing it; a later release signs it once Platform supports + // EvoNode shares and payouts + if (pos == Consensus::DEPLOYMENT_EVO_SHARES) continue; // IsValidMNActivation() is permissive for validation (it accepts bits of NEVER_ACTIVE deployments // and bits outside of any window), so only sign inside the deployment's own start/timeout window if (!IsSignalWindowOpen(deployment, time_past)) continue; diff --git a/src/node/miner.cpp b/src/node/miner.cpp index 3a64cb5d942c..7009839ab8bc 100644 --- a/src/node/miner.cpp +++ b/src/node/miner.cpp @@ -378,8 +378,7 @@ bool BlockAssembler::TestPackage(uint64_t packageSize, unsigned int packageSigOp // - safe TXs in regard to ChainLocks bool BlockAssembler::TestPackageTransactions(const CTxMemPool::setEntries& package) const { - const bool is_v24_active{ - DeploymentActiveAfter(m_chainstate.m_chain.Tip(), m_chainstate.m_chainman, Consensus::DEPLOYMENT_V24)}; + const SpecialTxRules special_tx_rules{GetSpecialTxRules(m_chainstate.m_chain.Tip(), m_chainstate.m_chainman)}; for (CTxMemPool::txiter it : package) { if (!IsFinalTx(it->GetTx(), nHeight, m_lock_time_cutoff)) { return false; @@ -396,7 +395,7 @@ bool BlockAssembler::TestPackageTransactions(const CTxMemPool::setEntries& packa // signature regardless, which is cheap enough here given how rare MNHF signals are. if (it->GetTx().IsSpecialTxVersion()) { TxValidationState tx_state; - if (!m_chain_helper.special_tx->CheckSpecialTx(it->GetTx(), m_chainstate.m_chain.Tip(), is_v24_active, + if (!m_chain_helper.special_tx->CheckSpecialTx(it->GetTx(), m_chainstate.m_chain.Tip(), special_tx_rules, m_chainstate.CoinsTip(), /*check_sigs=*/false, tx_state)) { return false; } @@ -406,8 +405,7 @@ bool BlockAssembler::TestPackageTransactions(const CTxMemPool::setEntries& packa // normal transaction creating or spending a template output can sit in the mempool across // v24 activation on a node accepting nonstandard transactions, and would poison every // template through the same TestBlockValidity path described above. - if (DeploymentActiveAfter(m_chainstate.m_chain.Tip(), m_chainstate.m_chainman, - Consensus::DEPLOYMENT_V24)) { + if (special_tx_rules.v24) { TxValidationState tx_state; if (!CheckSharedCollateralSpends(it->GetTx(), m_chainstate.CoinsTip(), tx_state) || !CheckSharedCollateralTemplateOutputs(it->GetTx(), tx_state)) { @@ -511,7 +509,7 @@ void BlockAssembler::addPackageTxs(const CTxMemPool& mempool, int& nPackagesSele // This map with signals is used only to find duplicates auto signals = m_chain_helper.ehf_manager->GetSignalsStage(pindexPrev); - const bool is_v24_active{DeploymentActiveAfter(pindexPrev, m_chainstate.m_chainman, Consensus::DEPLOYMENT_V24)}; + const SpecialTxRules special_tx_rules{GetSpecialTxRules(pindexPrev, m_chainstate.m_chainman)}; // mapModifiedTx will store sorted packages after they are modified // because some of their txs are already in the block @@ -651,7 +649,7 @@ void BlockAssembler::addPackageTxs(const CTxMemPool& mempool, int& nPackagesSele // producing an invalid template. if (IsAssetUnlockWithStableTxid(tx)) { TxValidationState state; - if (!m_chain_helper.special_tx->CheckSpecialTx(tx, m_chainstate.m_chain.Tip(), is_v24_active, + if (!m_chain_helper.special_tx->CheckSpecialTx(tx, m_chainstate.m_chain.Tip(), special_tx_rules, m_chainstate.CoinsTip(), /*check_sigs=*/true, state)) { LogPrintf("%s: package tx %s skipped, asset unlock instance not currently minable: %s\n", __func__, tx.GetHash().ToString(), state.ToString()); diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index 2fa3cbac1c1d..e09dc384984c 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -1687,6 +1687,7 @@ UniValue DeploymentInfo(const CBlockIndex* blockindex, const CMNHFManager::Signa } for (auto ehf_deploy : { /* sorted by activation block */ Consensus::DEPLOYMENT_V24, + Consensus::DEPLOYMENT_EVO_SHARES, Consensus::DEPLOYMENT_TESTDUMMY }) { SoftForkDescPushBack(blockindex, ehf_signals, softforks, chainman, ehf_deploy); } diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index e35183e739f1..b52709fc741b 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -326,6 +326,12 @@ static const CRPCConvertParam vRPCConvertParams[] = { "protx shared_register_prepare", 3, "coreP2PAddrs", true }, { "protx shared_register_prepare", 7, "earlyPeriodBlocks" }, { "protx shared_register_prepare", 8, "earlyPenalty" }, + { "protx shared_register_prepare_evo", 2, "shares" }, + { "protx shared_register_prepare_evo", 3, "coreP2PAddrs", true }, + { "protx shared_register_prepare_evo", 7, "earlyPeriodBlocks" }, + { "protx shared_register_prepare_evo", 8, "earlyPenalty" }, + { "protx shared_register_prepare_evo", 10, "platformP2PAddrs", true }, + { "protx shared_register_prepare_evo", 11, "platformHTTPSAddrs", true }, { "protx revoke", 3, "reason" }, { "protx revoke", 5, "submit" }, { "protx shared_combine", 2, "signatures" }, diff --git a/src/rpc/evo.cpp b/src/rpc/evo.cpp index ce043a8d9488..8db3f4f93e89 100644 --- a/src/rpc/evo.cpp +++ b/src/rpc/evo.cpp @@ -563,6 +563,16 @@ static interfaces::ProviderPlatformEndpoints ParsePlatformNetInfo(const UniValue strprintf("Invalid param for %s, must be a valid port [1-65535]", field_name)); } +static uint160 ParsePlatformNodeID(const UniValue& value) +{ + if (!IsHex(value.get_str())) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "platformNodeID must be hexadecimal string"); + } + uint160 platform_node_id; + platform_node_id.SetHex(value.get_str()); + return platform_node_id; +} + // forward declaration namespace { enum class ProTxRegisterAction @@ -912,12 +922,7 @@ static UniValue protx_register_common_wrapper(const JSONRPCRequest& request, typed_request.payouts = ParsePayouts(request.params[paramIdx + 5], "payouts"); if (mnType == MnType::Evo) { - if (!IsHex(request.params[paramIdx + 6].get_str())) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "platformNodeID must be hexadecimal string"); - } - uint160 platform_node_id; - platform_node_id.SetHex(request.params[paramIdx + 6].get_str()); - typed_request.platform_node_id = platform_node_id; + typed_request.platform_node_id = ParsePlatformNodeID(request.params[paramIdx + 6]); typed_request.net_info.platform_p2p = ParsePlatformNetInfo(request.params[paramIdx + 7], "platformP2PAddrs", capabilities.extended_addresses); typed_request.net_info.platform_https = ParsePlatformNetInfo(request.params[paramIdx + 8], "platformHTTPSAddrs", @@ -1062,12 +1067,7 @@ static UniValue protx_update_service_common_wrapper(const JSONRPCRequest& reques size_t paramIdx{3}; if (mnType == MnType::Evo) { const auto capabilities{evo::provider::GetCapabilities(node)}; - if (!IsHex(request.params[paramIdx].get_str())) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "platformNodeID must be hexadecimal string"); - } - uint160 platform_node_id; - platform_node_id.SetHex(request.params[paramIdx].get_str()); - typed_request.platform_node_id = platform_node_id; + typed_request.platform_node_id = ParsePlatformNodeID(request.params[paramIdx]); typed_request.net_info.platform_p2p = ParsePlatformNetInfo(request.params[paramIdx + 1], "platformP2PAddrs", capabilities.extended_addresses); typed_request.net_info.platform_https = ParsePlatformNetInfo(request.params[paramIdx + 2], "platformHTTPSAddrs", @@ -2156,26 +2156,10 @@ static RPCHelpMan evodb_repair() } #ifdef ENABLE_WALLET -static RPCHelpMan protx_shared_register_prepare() +static RPCHelpMan protx_shared_register_prepare_wrapper(const MnType mn_type) { - return RPCHelpMan{"protx shared_register_prepare", - "\nCreates an unsigned shared masternode registration (a version 3 ProRegTx with a collateral share\n" - "table) by appending the shared-collateral output and payload to a caller-supplied funding\n" - "transaction. The funding transaction must already contain every participant's contribution inputs\n" - "and any change outputs; the consent digest binds all of them. Every share owner must sign the\n" - "returned consent hash via \"protx shared_sign\"; combine with \"protx shared_combine\", then have the\n" - "funding inputs signed (signrawtransactionwithwallet) and broadcast with sendrawtransaction.\n" - "\nIMPORTANT: consent is required from each share in the table, not from each funding input; an input\n" - "is not tied to any share. Before signing its funding inputs, each participant should check the\n" - "share table (\"proRegTx\" in decoderawtransaction, or \"terms\" from shared_sign) for its own share,\n" - "with its own amount and addresses. Signing inputs into a table without that share gives the\n" - "contribution to the other shares.\n" - "\nIMPORTANT: once the registration confirms, every participant should create a zero-penalty\n" - "standby dissolution (\"protx shared_dissolve false false\") and store\n" - "the returned hex with their refund-key backup, separately from the share owner key. It becomes\n" - "valid when the early period ends (or immediately when no early period applies), then never expires;\n" - "broadcasting it after that point recovers the participant's principal without cooperation.\n", - { + const bool evo{mn_type == MnType::Evo}; + std::vector args{ {"fundingTx", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The serialized funding transaction with all participants' inputs and change outputs."}, {"shares", RPCArg::Type::ARR, RPCArg::Optional::NO, "The collateral share table, in consensus-significant order. Amounts must sum to the collateral.", { @@ -2195,7 +2179,46 @@ static RPCHelpMan protx_shared_register_prepare() RPCArgOptions{.skip_type_check = true}}, {"earlyPeriodBlocks", RPCArg::Type::NUM, RPCArg::Optional::NO, "Length in blocks of the early period during which unilateral dissolution is penalized (up to 420480)."}, {"earlyPenalty", RPCArg::Type::NUM, RPCArg::Optional::NO, "Penalty in duffs for unilateral dissolution during the early period (must be below the smallest share, and zero when earlyPeriodBlocks is zero)."}, - }, + }; + if (evo) { + args.push_back(GetRpcArg("platformNodeID")); + args.push_back({"platformP2PAddrs", RPCArg::Type::ARR, RPCArg::Optional::NO, + "Array of addresses in the form \"ADDR:PORT\" used by Platform for peer-to-peer connection.\n" + "Must be unique on the network. Can be set to an empty string, which will require a ProUpServTx afterwards.", + { + {"address", RPCArg::Type::STR, RPCArg::Optional::NO, ""}, + }, + RPCArgOptions{.skip_type_check = true}}); + args.push_back({"platformHTTPSAddrs", RPCArg::Type::ARR, RPCArg::Optional::NO, + "Array of addresses in the form \"ADDR:PORT\" used by Platform for their HTTPS API.\n" + "Must be unique on the network. Can be set to an empty string, which will require a ProUpServTx afterwards.", + { + {"address", RPCArg::Type::STR, RPCArg::Optional::NO, ""}, + }, + RPCArgOptions{.skip_type_check = true}}); + } + const std::string example_args{"\"fundingTx\" \"[...]\" \"1.2.3.4:1234\" \"operatorPubKey\" \"" + EXAMPLE_ADDRESS[1] + + "\" 0 10000 5000000000"}; + return RPCHelpMan{evo ? "protx shared_register_prepare_evo" : "protx shared_register_prepare", + std::string{evo ? "\nSame as \"protx shared_register_prepare\", but for an EvoNode. EvoNodes can only register with\n" + "shared collateral once the evo_shares deployment is active.\n" : ""} + + "\nCreates an unsigned shared masternode registration (a version 3 ProRegTx with a collateral share\n" + "table) by appending the shared-collateral output and payload to a caller-supplied funding\n" + "transaction. The funding transaction must already contain every participant's contribution inputs\n" + "and any change outputs; the consent digest binds all of them. Every share owner must sign the\n" + "returned consent hash via \"protx shared_sign\"; combine with \"protx shared_combine\", then have the\n" + "funding inputs signed (signrawtransactionwithwallet) and broadcast with sendrawtransaction.\n" + "\nIMPORTANT: consent is required from each share in the table, not from each funding input; an input\n" + "is not tied to any share. Before signing its funding inputs, each participant should check the\n" + "share table (\"proRegTx\" in decoderawtransaction, or \"terms\" from shared_sign) for its own share,\n" + "with its own amount and addresses. Signing inputs into a table without that share gives the\n" + "contribution to the other shares.\n" + "\nIMPORTANT: once the registration confirms, every participant should create a zero-penalty\n" + "standby dissolution (\"protx shared_dissolve false false\") and store\n" + "the returned hex with their refund-key backup, separately from the share owner key. It becomes\n" + "valid when the early period ends (or immediately when no early period applies), then never expires;\n" + "broadcasting it after that point recovers the participant's principal without cooperation.\n", + args, RPCResult{RPCResult::Type::OBJ, "", "", { {RPCResult::Type::STR_HEX, "tx", "The serialized unsigned shared ProRegTx"}, @@ -2204,12 +2227,14 @@ static RPCHelpMan protx_shared_register_prepare() CProRegTx::GetJsonHelp("terms", /*optional=*/false), {RPCResult::Type::STR, "warning", /*optional=*/true, "Present when earlyPenalty is zero: any participant can force an early exit at no cost beyond the transaction fee"}, }}, - RPCExamples{HelpExampleCli("protx", "shared_register_prepare \"fundingTx\" \"[...]\" \"1.2.3.4:1234\" \"operatorPubKey\" \"" + EXAMPLE_ADDRESS[1] + "\" 0 10000 5000000000")}, - [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue + RPCExamples{evo ? HelpExampleCli("protx", "shared_register_prepare_evo " + example_args + " \"f2dbd9b0a1f541a7c44d34a58674d0262f5feca5\" \"[\\\"1.2.3.4:22821\\\"]\" \"[\\\"1.2.3.4:22822\\\"]\"") + : HelpExampleCli("protx", "shared_register_prepare " + example_args)}, + [mn_type](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { NodeContext& node = EnsureAnyNodeContext(request.context); interfaces::SharedRegistrationRequest typed_request; + typed_request.type = mn_type; if (!DecodeHexTx(typed_request.funding_tx, request.params[0].get_str())) { throw JSONRPCError(RPC_INVALID_PARAMETER, "funding transaction not deserializable"); } @@ -2239,6 +2264,14 @@ static RPCHelpMan protx_shared_register_prepare() typed_request.early_period_blocks = static_cast(earlyPeriodBlocks); typed_request.early_penalty = request.params[7].getInt(); + if (mn_type == MnType::Evo) { + typed_request.platform_node_id = ParsePlatformNodeID(request.params[8]); + typed_request.net_info.platform_p2p = ParsePlatformNetInfo(request.params[9], "platformP2PAddrs", + /*extended_addresses=*/true); + typed_request.net_info.platform_https = ParsePlatformNetInfo(request.params[10], "platformHTTPSAddrs", + /*extended_addresses=*/true); + } + const auto prepared{UnwrapOrThrow(evo::provider::PrepareSharedRegistration(node, typed_request))}; UniValue ret(UniValue::VOBJ); ret.pushKV("tx", EncodeHexTx(*prepared.tx)); @@ -2253,6 +2286,16 @@ static RPCHelpMan protx_shared_register_prepare() }; } +static RPCHelpMan protx_shared_register_prepare() +{ + return protx_shared_register_prepare_wrapper(MnType::Regular); +} + +static RPCHelpMan protx_shared_register_prepare_evo() +{ + return protx_shared_register_prepare_wrapper(MnType::Evo); +} + static RPCHelpMan protx_shared_dissolve_prepare() { return RPCHelpMan{"protx shared_dissolve_prepare", @@ -2317,6 +2360,7 @@ static RPCHelpMan protx_help() " update_registrar_legacy - (DEPRECATED) Create ProUpRegTx by parsing BLS using the legacy scheme, then send it to network\n" " revoke - Create and send ProUpRevTx to network\n" " shared_register_prepare - Create an unsigned shared masternode ProTx\n" + " shared_register_prepare_evo - Create an unsigned shared masternode ProTx for an EvoNode\n" " shared_sign - Sign a shared masternode transaction with this wallet's share owner keys\n" " shared_combine - Combine share owner signatures into a shared masternode transaction\n" " shared_dissolve - Create, sign and send a unilateral ProDisTx\n" @@ -2448,6 +2492,7 @@ Span GetWalletEvoRPCCommands() {"evo", &protx_update_registrar}, {"evo", &protx_revoke}, {"evo", &protx_shared_register_prepare}, + {"evo", &protx_shared_register_prepare_evo}, {"evo", &protx_shared_sign}, {"evo", &protx_shared_combine}, {"evo", &protx_shared_dissolve}, diff --git a/src/test/evo_deterministicmns_tests.cpp b/src/test/evo_deterministicmns_tests.cpp index f361bf5b46e5..8b2f1e888cd1 100644 --- a/src/test/evo_deterministicmns_tests.cpp +++ b/src/test/evo_deterministicmns_tests.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -45,6 +46,11 @@ static bool IsV24Active(const ChainstateManager& chainman) EXCLUSIVE_LOCKS_REQUI return DeploymentActiveAfter(chainman.ActiveChain().Tip(), chainman, Consensus::DEPLOYMENT_V24); } +static SpecialTxRules TipRules(const ChainstateManager& chainman) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) +{ + return GetSpecialTxRules(chainman.ActiveChain().Tip(), chainman); +} + static CMutableTransaction CreateSpendTx(const ChainstateManager& chainman, SimpleUTXOMap& utxos, const CScript& scriptPayout, CAmount amount, const CKey& coinbaseKey) { CMutableTransaction tx; @@ -530,8 +536,8 @@ void FuncProUpRegTxV3OnLegacyValid(TestChainSetup& setup) { LOCK(cs_main); BOOST_CHECK(CheckProUpRegTx(CTransaction(tx), chainman.ActiveChain().Tip(), dmnman, - chainman.ActiveChainstate().CoinsTip(), chainman.GetConsensus(), - IsV24Active(chainman), val_state, /*check_sigs=*/true)); + chainman.ActiveChainstate().CoinsTip(), chainman.GetConsensus(), TipRules(chainman), + val_state, /*check_sigs=*/true)); } BOOST_CHECK(val_state.IsValid()); @@ -611,7 +617,7 @@ void FuncProUpRegTxV2CannotBypassV3PayoutCollateralReuse(TestChainSetup& setup) LOCK(cs_main); BOOST_CHECK(!CheckProUpRegTx(CTransaction(tx_upreg), chainman.ActiveChain().Tip(), dmnman, chainman.ActiveChainstate().CoinsTip(), chainman.GetConsensus(), - IsV24Active(chainman), val_state, /*check_sigs=*/true)); + TipRules(chainman), val_state, /*check_sigs=*/true)); } BOOST_CHECK_EQUAL(val_state.GetRejectReason(), "bad-protx-payee-reuse"); } @@ -887,7 +893,7 @@ void FuncProRegTxRejectsInvalidDeserializedExtNetInfo(TestChainSetup& setup) LOCK(cs_main); BOOST_CHECK(!CheckProRegTx(BuildExtNetInfoProRegTx(std::move(net_info)), chainman.ActiveChain().Tip(), dmnman, chainman.ActiveChainstate().CoinsTip(), chainman.GetConsensus(), - IsV24Active(chainman), state, + TipRules(chainman), state, /*check_sigs=*/false)); } BOOST_CHECK_EQUAL(state.GetResult(), TxValidationResult::TX_BAD_SPECIAL); @@ -954,10 +960,10 @@ void FuncDIP3Protx(TestChainSetup& setup) LOCK(cs_main); BOOST_REQUIRE(CheckProRegTx(CTransaction(tx), chainman.ActiveChain().Tip(), dmnman, chainman.ActiveChainstate().CoinsTip(), chainman.GetConsensus(), - IsV24Active(chainman), dummy_state, true)); + TipRules(chainman), dummy_state, true)); BOOST_REQUIRE(CheckProRegTx(CTransaction(tx2), chainman.ActiveChain().Tip(), dmnman, chainman.ActiveChainstate().CoinsTip(), chainman.GetConsensus(), - IsV24Active(chainman), dummy_state, true)); + TipRules(chainman), dummy_state, true)); } // But the signature should not verify anymore BOOST_REQUIRE(CheckTransactionSignature(tx, coins)); @@ -1065,10 +1071,10 @@ void FuncDIP3Protx(TestChainSetup& setup) LOCK(cs_main); BOOST_REQUIRE(CheckProUpRegTx(CTransaction(tx), chainman.ActiveChain().Tip(), dmnman, chainman.ActiveChainstate().CoinsTip(), chainman.GetConsensus(), - IsV24Active(chainman), dummy_state, true)); + TipRules(chainman), dummy_state, true)); BOOST_REQUIRE(!CheckProUpRegTx(CTransaction(tx2), chainman.ActiveChain().Tip(), dmnman, chainman.ActiveChainstate().CoinsTip(), chainman.GetConsensus(), - IsV24Active(chainman), dummy_state, true)); + TipRules(chainman), dummy_state, true)); } BOOST_REQUIRE(CheckTransactionSignature(tx, coins)); BOOST_REQUIRE(!CheckTransactionSignature(tx2, coins)); @@ -1945,7 +1951,7 @@ void FuncMigrationRejectedWhenKeySquatted(TestChainV24SignalBeforeV19Setup& setu LOCK(cs_main); BOOST_CHECK(!CheckProUpRegTx(CTransaction(tx), chainman.ActiveChain().Tip(), dmnman, chainman.ActiveChainstate().CoinsTip(), chainman.GetConsensus(), - IsV24Active(chainman), st, true)); + TipRules(chainman), st, true)); BOOST_CHECK_EQUAL(st.GetRejectReason(), "bad-protx-dup-key"); } }; @@ -2196,14 +2202,16 @@ static uint256 RegisterBasicEvoNode(TestChainV24SignalBeforeV19Setup& setup, con } static CMutableTransaction CreateExtAddrProUpRegTx(TestChainV24SignalBeforeV19Setup& setup, const uint256& proTxHash, - const CKey& owner_key, const CBLSSecretKey& operator_key) + const CKey& owner_key, const CBLSSecretKey& operator_key, + MasternodePayoutShares payouts = { + {GenerateRandomAddress(), MasternodePayoutShare::MAX_REWARD}}) { CProUpRegTx proTx; proTx.nVersion = ProTxVersion::ExtAddr; proTx.proTxHash = proTxHash; proTx.pubKeyOperator.Set(operator_key.GetPublicKey(), /*specificLegacyScheme=*/false); proTx.keyIDVoting = owner_key.GetPubKey().GetID(); - proTx.payouts = {{GenerateRandomAddress(), MasternodePayoutShare::MAX_REWARD}}; + proTx.payouts = std::move(payouts); CMutableTransaction tx; tx.nVersion = 3; tx.nType = TRANSACTION_PROVIDER_UPDATE_REGISTRAR; @@ -2269,7 +2277,7 @@ void FuncEvoNodeRaisedToExtAddrByServiceUpdateOnly(TestChainV24SignalBeforeV19Se TxValidationState val_state; LOCK(cs_main); CheckProUpRegTx(CTransaction(tx), chainman.ActiveChain().Tip(), dmnman, chainman.ActiveChainstate().CoinsTip(), - chainman.GetConsensus(), IsV24Active(chainman), val_state, /*check_sigs=*/true); + chainman.GetConsensus(), TipRules(chainman), val_state, /*check_sigs=*/true); return val_state.GetRejectReason(); }; auto check_proupserv = [&](const CMutableTransaction& tx) { @@ -2341,7 +2349,7 @@ void FuncEvoNodeRegistrarRaiseNotMined(TestChainV24SignalBeforeV19Setup& setup) LOCK(cs_main); BOOST_CHECK(!CheckProUpRegTx(CTransaction(tx), chainman.ActiveChain().Tip(), dmnman, chainman.ActiveChainstate().CoinsTip(), chainman.GetConsensus(), - IsV24Active(chainman), val_state, /*check_sigs=*/true)); + TipRules(chainman), val_state, /*check_sigs=*/true)); BOOST_CHECK_EQUAL(val_state.GetRejectReason(), "bad-protx-netinfo-version"); } @@ -2367,6 +2375,151 @@ BOOST_AUTO_TEST_CASE(evonode_registrar_raise_not_mined) FuncEvoNodeRegistrarRaiseNotMined(setup); } +// An ExtAddr registration with internal collateral, shared when shares are given. Its funding input is a +// placeholder: CheckProRegTx() does not look the funding inputs up. +static CTransaction CreateExtAddrProRegTx(MnType type, const MasternodePayoutShares& payouts, const CollateralShares& shares) +{ + CBLSSecretKey operator_key; + operator_key.MakeNewKey(); + + CProRegTx pro_reg; + pro_reg.nVersion = ProTxVersion::ExtAddr; + pro_reg.nType = type; + pro_reg.netInfo = NetInfoInterface::MakeNetInfo(pro_reg.nVersion); + pro_reg.collateralOutpoint.n = 0; + pro_reg.pubKeyOperator.Set(operator_key.GetPublicKey(), /*specificLegacyScheme=*/false); + pro_reg.keyIDVoting = GenerateRandomKey().GetPubKey().GetID(); + if (type == MnType::Evo) { + pro_reg.platformNodeID = uint160{Span{GetRandHash()}.first(uint160::size())}; + } + if (shares.empty()) { + pro_reg.keyIDOwner = GenerateRandomKey().GetPubKey().GetID(); + pro_reg.payouts = payouts; + } else { + pro_reg.shares = shares; + pro_reg.vchJoinSigs.resize(shares.size()); + } + + CMutableTransaction tx; + tx.nVersion = 3; + tx.nType = TRANSACTION_PROVIDER_REGISTER; + tx.vin.emplace_back(COutPoint(GetRandHash(), 0)); + tx.vout.emplace_back(GetMnType(type).collat_amount, + shares.empty() ? GenerateRandomAddress() : SharedCollateralScript()); + pro_reg.inputsHash = CalcTxInputsHash(CTransaction(tx)); + SetTxPayload(tx, pro_reg); + return CTransaction(tx); +} + +// Until evo_shares activates, an EvoNode registers with a single owner payout and without shared +// collateral. Regular masternodes are not restricted. +void FuncEvoNodeRegistrationNeedsEvoShares(TestChainV24SignalBeforeV19Setup& setup) +{ + auto& chainman = setup.chainman; + auto& dmnman = setup.dmnman; + + setup.MineToV19(); + setup.MineToV24(); + + const MasternodePayoutShares one_payout{{GenerateRandomAddress(), MasternodePayoutShare::MAX_REWARD}}; + const MasternodePayoutShares two_payouts{{GenerateRandomAddress(), 6000}, {GenerateRandomAddress(), 4000}}; + const auto two_shares = [](MnType type) { + const CAmount collateral{GetMnType(type).collat_amount}; + return CollateralShares{{collateral - 100 * COIN, GenerateRandomAddress(), CScript{}, + GenerateRandomKey().GetPubKey().GetID()}, + {100 * COIN, GenerateRandomAddress(), CScript{}, GenerateRandomKey().GetPubKey().GetID()}}; + }; + + struct Case { + std::string name; + MnType type; + MasternodePayoutShares payouts; + CollateralShares shares; + bool evo_shares; + std::string expected; + }; + const std::vector cases{ + {"evo, one payout", MnType::Evo, one_payout, {}, false, ""}, + {"evo, two payouts", MnType::Evo, two_payouts, {}, false, "bad-protx-payouts-evo"}, + {"evo, two payouts, evo_shares", MnType::Evo, two_payouts, {}, true, ""}, + {"regular, two payouts", MnType::Regular, two_payouts, {}, false, ""}, + {"evo, shared", MnType::Evo, {}, two_shares(MnType::Evo), false, "bad-protx-shares-evo"}, + {"evo, shared, evo_shares", MnType::Evo, {}, two_shares(MnType::Evo), true, ""}, + {"regular, shared", MnType::Regular, {}, two_shares(MnType::Regular), false, ""}, + }; + for (const auto& c : cases) { + const auto tx{CreateExtAddrProRegTx(c.type, c.payouts, c.shares)}; + TxValidationState state; + LOCK(cs_main); + SpecialTxRules rules{TipRules(chainman)}; + BOOST_REQUIRE(rules.v24 && !rules.evo_shares); + rules.evo_shares = c.evo_shares; + CheckProRegTx(tx, chainman.ActiveChain().Tip(), dmnman, chainman.ActiveChainstate().CoinsTip(), + chainman.GetConsensus(), rules, state, /*check_sigs=*/false); + BOOST_CHECK_MESSAGE(state.GetRejectReason() == c.expected, + strprintf("%s: got '%s'", c.name, state.GetRejectReason())); + } +} + +BOOST_AUTO_TEST_CASE(evonode_registration_needs_evo_shares) +{ + TestChainV24SignalBeforeV19Setup setup; + FuncEvoNodeRegistrationNeedsEvoShares(setup); +} + +// Until evo_shares activates, a registrar update keeps an EvoNode at a single owner payout, also when it +// rotates the operator key +void FuncEvoNodePayoutUpdateNeedsEvoShares(TestChainV24SignalBeforeV19Setup& setup) +{ + auto& chainman = setup.chainman; + auto& dmnman = setup.dmnman; + + setup.MineToV19(); + CKey owner_key; + owner_key.MakeNewKey(true); + CBLSSecretKey operator_key; + operator_key.MakeNewKey(); + CBLSSecretKey new_operator_key; + new_operator_key.MakeNewKey(); + const auto proTxHash = RegisterBasicEvoNode(setup, "1.1.1.6", 443, owner_key, operator_key); + setup.MineToV24(); + setup.ProcessBlock({CreateEvoProUpServTx(setup, proTxHash, ProTxVersion::ExtAddr, "1.1.1.6", operator_key)}); + + const MasternodePayoutShares two_payouts{{GenerateRandomAddress(), 6000}, {GenerateRandomAddress(), 4000}}; + struct Case { + std::string name; + const CBLSSecretKey& operator_key; + MasternodePayoutShares payouts; + bool evo_shares; + std::string expected; + }; + const std::vector cases{ + {"one payout", operator_key, {{GenerateRandomAddress(), MasternodePayoutShare::MAX_REWARD}}, false, ""}, + {"two payouts", operator_key, two_payouts, false, "bad-protx-payouts-evo"}, + {"two payouts, operator key rotation", new_operator_key, two_payouts, false, "bad-protx-payouts-evo"}, + {"two payouts, evo_shares", operator_key, two_payouts, true, ""}, + {"two payouts, operator key rotation, evo_shares", new_operator_key, two_payouts, true, ""}, + }; + for (const auto& c : cases) { + const auto tx{CreateExtAddrProUpRegTx(setup, proTxHash, owner_key, c.operator_key, c.payouts)}; + TxValidationState state; + LOCK(cs_main); + SpecialTxRules rules{TipRules(chainman)}; + BOOST_REQUIRE(rules.v24 && !rules.evo_shares); + rules.evo_shares = c.evo_shares; + CheckProUpRegTx(CTransaction(tx), chainman.ActiveChain().Tip(), dmnman, chainman.ActiveChainstate().CoinsTip(), + chainman.GetConsensus(), rules, state, /*check_sigs=*/true); + BOOST_CHECK_MESSAGE(state.GetRejectReason() == c.expected, + strprintf("%s: got '%s'", c.name, state.GetRejectReason())); + } +} + +BOOST_AUTO_TEST_CASE(evonode_payout_update_needs_evo_shares) +{ + TestChainV24SignalBeforeV19Setup setup; + FuncEvoNodePayoutUpdateNeedsEvoShares(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 @@ -2406,11 +2559,11 @@ void FuncSameMnSameBlockVersionCrossingKeyRotation(TestChainV24SignalBeforeV19Se TxValidationState s1, s2; BOOST_REQUIRE_MESSAGE(CheckProUpRegTx(CTransaction(tx1), chainman.ActiveChain().Tip(), dmnman, chainman.ActiveChainstate().CoinsTip(), chainman.GetConsensus(), - IsV24Active(chainman), s1, true), + TipRules(chainman), s1, true), "tx1 rejected standalone: " << s1.GetRejectReason()); BOOST_REQUIRE_MESSAGE(CheckProUpRegTx(CTransaction(tx2), chainman.ActiveChain().Tip(), dmnman, chainman.ActiveChainstate().CoinsTip(), chainman.GetConsensus(), - IsV24Active(chainman), s2, true), + TipRules(chainman), s2, true), "tx2 rejected standalone: " << s2.GetRejectReason()); } @@ -2514,11 +2667,11 @@ void FuncSameMnSameBlockMigrationConsistent(TestChainV24SignalBeforeV19Setup& se TxValidationState s1, s2; BOOST_REQUIRE_MESSAGE(CheckProUpRegTx(CTransaction(tx1), chainman.ActiveChain().Tip(), dmnman, chainman.ActiveChainstate().CoinsTip(), chainman.GetConsensus(), - IsV24Active(chainman), s1, true), + TipRules(chainman), s1, true), "tx1 rejected standalone: " << s1.GetRejectReason()); BOOST_REQUIRE_MESSAGE(CheckProUpRegTx(CTransaction(tx2), chainman.ActiveChain().Tip(), dmnman, chainman.ActiveChainstate().CoinsTip(), chainman.GetConsensus(), - IsV24Active(chainman), s2, true), + TipRules(chainman), s2, true), "tx2 rejected standalone: " << s2.GetRejectReason()); } @@ -2718,11 +2871,11 @@ void FuncSameBlockCrossSchemeKeyPairRejected(TestChainV24SignalBeforeV19Setup& s TxValidationState s1, s2; BOOST_REQUIRE_MESSAGE(CheckProUpRegTx(CTransaction(tx1), chainman.ActiveChain().Tip(), dmnman, chainman.ActiveChainstate().CoinsTip(), chainman.GetConsensus(), - IsV24Active(chainman), s1, true), + TipRules(chainman), s1, true), "tx1 rejected standalone: " << s1.GetRejectReason()); BOOST_REQUIRE_MESSAGE(CheckProUpRegTx(CTransaction(tx2), chainman.ActiveChain().Tip(), dmnman, chainman.ActiveChainstate().CoinsTip(), chainman.GetConsensus(), - IsV24Active(chainman), s2, true), + TipRules(chainman), s2, true), "tx2 rejected standalone: " << s2.GetRejectReason()); } @@ -2829,7 +2982,7 @@ void FuncMempoolRejectsCrossSchemeKeyRace(TestChainV24SignalBeforeV19Setup& setu LOCK(cs_main); BOOST_REQUIRE_MESSAGE(CheckProUpRegTx(CTransaction(tx_second), chainman.ActiveChain().Tip(), dmnman, chainman.ActiveChainstate().CoinsTip(), chainman.GetConsensus(), - IsV24Active(chainman), st, + TipRules(chainman), st, /*check_sigs=*/true), "second claim should still pass its consensus check: " << st.GetRejectReason()); } @@ -2903,8 +3056,8 @@ void FuncProUpRegTxRejectsCrossSchemeKeyReuse(TestChainV24SignalBeforeV19Setup& auto check_upreg = [&](const CMutableTransaction& tx, TxValidationState& st) { LOCK(cs_main); return CheckProUpRegTx(CTransaction(tx), chainman.ActiveChain().Tip(), dmnman, - chainman.ActiveChainstate().CoinsTip(), chainman.GetConsensus(), IsV24Active(chainman), - st, /*check_sigs=*/true); + chainman.ActiveChainstate().CoinsTip(), chainman.GetConsensus(), TipRules(chainman), st, + /*check_sigs=*/true); }; // MN-A tries to take MN-B's key under the OTHER encoding, via a v1 payload. This is the reverse @@ -2979,7 +3132,7 @@ void FuncProRegTxRejectsCrossSchemeKeyReuse(TestChainV24SignalBeforeV19Setup& se auto check_proreg = [&](const CMutableTransaction& tx, TxValidationState& st) { LOCK(cs_main); return CheckProRegTx(CTransaction(tx), chainman.ActiveChain().Tip(), dmnman, - chainman.ActiveChainstate().CoinsTip(), chainman.GetConsensus(), IsV24Active(chainman), st, + chainman.ActiveChainstate().CoinsTip(), chainman.GetConsensus(), TipRules(chainman), st, /*check_sigs=*/true); }; @@ -3131,7 +3284,7 @@ void FuncPreV24BehaviourUnchanged(TestChainSetup& setup) LOCK(cs_main); BOOST_CHECK_MESSAGE(CheckProUpRegTx(CTransaction(tx_upreg), chainman.ActiveChain().Tip(), dmnman, chainman.ActiveChainstate().CoinsTip(), chainman.GetConsensus(), - IsV24Active(chainman), val_state, + TipRules(chainman), val_state, /*check_sigs=*/true), "pre-v24 same-key ProUpRegTx wrongly rejected: " << val_state.GetRejectReason()); } @@ -3199,7 +3352,7 @@ void FuncStaleSpecialTxDoesNotPoisonTemplate(TestChainV24SignalBeforeV19Setup& s LOCK(cs_main); BOOST_REQUIRE(!CheckProRegTx(CTransaction(tx_stale), chainman.ActiveChain().Tip(), dmnman, chainman.ActiveChainstate().CoinsTip(), chainman.GetConsensus(), - IsV24Active(chainman), val_state, + TipRules(chainman), val_state, /*check_sigs=*/true)); BOOST_REQUIRE_EQUAL(val_state.GetRejectReason(), "bad-protx-dup-key"); } @@ -3255,7 +3408,7 @@ void FuncProUpRegTxMigratesLegacySameKey(TestChainV24SignalBeforeV19Setup& setup LOCK(cs_main); BOOST_REQUIRE_MESSAGE(CheckProUpRegTx(CTransaction(tx), chainman.ActiveChain().Tip(), dmnman, chainman.ActiveChainstate().CoinsTip(), chainman.GetConsensus(), - IsV24Active(chainman), val_state, + TipRules(chainman), val_state, /*check_sigs=*/true), "same-key migration rejected: " << val_state.GetRejectReason()); } @@ -3285,7 +3438,7 @@ void FuncProUpRegTxMigratesLegacySameKey(TestChainV24SignalBeforeV19Setup& setup LOCK(cs_main); BOOST_REQUIRE_MESSAGE(CheckProUpRegTx(CTransaction(tx), chainman.ActiveChain().Tip(), dmnman, chainman.ActiveChainstate().CoinsTip(), chainman.GetConsensus(), - IsV24Active(chainman), val_state, + TipRules(chainman), val_state, /*check_sigs=*/true), "new-key rotation rejected: " << val_state.GetRejectReason()); } diff --git a/src/test/evo_sharedmn_tests.cpp b/src/test/evo_sharedmn_tests.cpp index 5978d835ad85..cf6719b120c5 100644 --- a/src/test/evo_sharedmn_tests.cpp +++ b/src/test/evo_sharedmn_tests.cpp @@ -212,11 +212,6 @@ BOOST_AUTO_TEST_CASE(shared_proregtx_shape_validation) return proTx; }; - { - auto proTx = base(); - proTx.nType = MnType::Evo; - check(proTx, "bad-protx-shares-evo"); - } { auto proTx = base(); proTx.collateralOutpoint.hash = GetRandHash(); @@ -500,6 +495,21 @@ BOOST_AUTO_TEST_CASE(shared_reg_consent_hash) mutated.nType = MnType::Evo; BOOST_CHECK(mutated.MakeSharedRegConsentHash(CTransaction(mtx)) != base); } + const uint160 platform_node_id{std::vector(20, 0x01)}; + { + // An EvoNode's Platform node ID is covered + CProRegTx evo{proTx}; + evo.nType = MnType::Evo; + const uint256 evo_base = evo.MakeSharedRegConsentHash(CTransaction(mtx)); + evo.platformNodeID = platform_node_id; + BOOST_CHECK(evo.MakeSharedRegConsentHash(CTransaction(mtx)) != evo_base); + } + { + // A regular masternode has no Platform node ID, and its digest is unchanged by the field + CProRegTx mutated{proTx}; + mutated.platformNodeID = platform_node_id; + BOOST_CHECK(mutated.MakeSharedRegConsentHash(CTransaction(mtx)) == base); + } { CProRegTx mutated{proTx}; CBLSSecretKey sk; diff --git a/src/test/evo_snapshot_tests.cpp b/src/test/evo_snapshot_tests.cpp index 2d3f97647c16..38d18473d454 100644 --- a/src/test/evo_snapshot_tests.cpp +++ b/src/test/evo_snapshot_tests.cpp @@ -1192,10 +1192,10 @@ BOOST_FIXTURE_TEST_CASE(mnhf_signal_wire_order_is_canonical, BasicTestingSetup) BOOST_FIXTURE_TEST_CASE(mnhf_signals_outnumbering_deployments_are_valid, BasicTestingSetup) { // CMNHFManager drops a signal only when a current deployment reuses its - // bit, so signals of buried EHF forks (MN_RR bit 10, WITHDRAWALS bit 11) - // stay in the map alongside V24's bit 12. + // bit, so signals of buried EHF forks (bits 10, 11 and 12) stay in the + // map alongside V24's bit 13. auto snapshot{SyntheticSnapshot()}; - snapshot.mnhf_signals = {{10, 100}, {11, 200}, {12, 300}}; + snapshot.mnhf_signals = {{10, 100}, {11, 200}, {12, 300}, {13, 400}}; BOOST_REQUIRE_GT(snapshot.mnhf_signals.size(), size_t{Consensus::MAX_VERSION_BITS_DEPLOYMENTS}); BOOST_CHECK_NO_THROW(snapshot.Validate()); diff --git a/src/test/versionbits_tests.cpp b/src/test/versionbits_tests.cpp index 1cd9db5fef07..6770f1d76a97 100644 --- a/src/test/versionbits_tests.cpp +++ b/src/test/versionbits_tests.cpp @@ -476,4 +476,26 @@ BOOST_AUTO_TEST_CASE(versionbits_computeblockversion) } } +BOOST_AUTO_TEST_CASE(evo_shares_follows_v24) +{ + // evo_shares lifts restrictions that v24 introduces. Its consensus parameters ship in v24 and must + // stay identical in the release that signals for it, so they mirror v24's on every network. + for (const auto& chain_name : {CBaseChainParams::MAIN, CBaseChainParams::TESTNET, CBaseChainParams::DEVNET, + CBaseChainParams::REGTEST}) { + BOOST_TEST_MESSAGE(chain_name); + const auto chainParams = CreateChainParams(*m_node.args, chain_name); + const auto& v24{chainParams->GetConsensus().vDeployments[Consensus::DEPLOYMENT_V24]}; + const auto& evo_shares{chainParams->GetConsensus().vDeployments[Consensus::DEPLOYMENT_EVO_SHARES]}; + BOOST_CHECK_EQUAL(evo_shares.bit, 14); + BOOST_CHECK_EQUAL(evo_shares.nStartTime, v24.nStartTime); + BOOST_CHECK_EQUAL(evo_shares.nTimeout, v24.nTimeout); + BOOST_CHECK_EQUAL(evo_shares.min_activation_height, v24.min_activation_height); + BOOST_CHECK_EQUAL(evo_shares.nWindowSize, v24.nWindowSize); + BOOST_CHECK_EQUAL(evo_shares.nThresholdStart, v24.nThresholdStart); + BOOST_CHECK_EQUAL(evo_shares.nThresholdMin, v24.nThresholdMin); + BOOST_CHECK_EQUAL(evo_shares.nFalloffCoeff, v24.nFalloffCoeff); + BOOST_CHECK(evo_shares.useEHF && v24.useEHF); + } +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/validation.cpp b/src/validation.cpp index 397626256a13..3e20f8617c8b 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -734,7 +734,7 @@ class MemPoolAccept // so it is validated against its window instead of the tip: nodes that did not hold it (after a // restart, or a new peer) admit it and its descendants too. It stays unminable and is not // InstantSend-locked until refreshed, as the miner and the lock signer validate against the tip. - bool CheckSpecialTxForMempool(const CTransaction& tx, bool is_v24_active, TxValidationState& state) + bool CheckSpecialTxForMempool(const CTransaction& tx, SpecialTxRules rules, TxValidationState& state) EXCLUSIVE_LOCKS_REQUIRED(cs_main); // Enforce package mempool ancestor/descendant limits (distinct from individual @@ -799,12 +799,12 @@ class MemPoolAccept CTxMemPool::Limits m_limits; }; -bool MemPoolAccept::CheckSpecialTxForMempool(const CTransaction& tx, bool is_v24_active, TxValidationState& state) +bool MemPoolAccept::CheckSpecialTxForMempool(const CTransaction& tx, SpecialTxRules rules, TxValidationState& state) { const CChain& chain{m_active_chainstate.m_chain}; const auto check_at = [&](const CBlockIndex* pindex, TxValidationState& check_state) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { - return m_chain_helper.special_tx->CheckSpecialTx(tx, pindex, is_v24_active, m_active_chainstate.CoinsTip(), + return m_chain_helper.special_tx->CheckSpecialTx(tx, pindex, rules, m_active_chainstate.CoinsTip(), /*check_sigs=*/true, check_state); }; const auto payload{IsAssetUnlockWithStableTxid(tx) ? GetTxPayload(tx) : std::nullopt}; @@ -1100,8 +1100,7 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws) // mined // NOTE: we use UTXO here and do NOT allow mempool txes as masternode collaterals const CBlockIndex* tip{m_active_chainstate.m_chain.Tip()}; - const bool is_v24_active{DeploymentActiveAfter(tip, m_active_chainstate.m_chainman, Consensus::DEPLOYMENT_V24)}; - if (!CheckSpecialTxForMempool(tx, is_v24_active, state)) return false; + if (!CheckSpecialTxForMempool(tx, GetSpecialTxRules(tip, m_active_chainstate.m_chainman), state)) return false; if (m_pool.existsProviderTxConflict(tx)) { return state.Invalid(TxValidationResult::TX_CONFLICT, "protx-dup"); @@ -1398,8 +1397,8 @@ std::optional MemPoolAccept::TryAssetUnlockRefresh(const CT // and quorum signature. Everything the txid covers is identical to the held instance and // was validated when it was admitted. const CBlockIndex* tip{m_active_chainstate.m_chain.Tip()}; - const bool is_v24_active{DeploymentActiveAfter(tip, m_active_chainstate.m_chainman, Consensus::DEPLOYMENT_V24)}; - if (!m_chain_helper.special_tx->CheckSpecialTx(*ptx, tip, is_v24_active, m_active_chainstate.CoinsTip(), + if (!m_chain_helper.special_tx->CheckSpecialTx(*ptx, tip, GetSpecialTxRules(tip, m_active_chainstate.m_chainman), + m_active_chainstate.CoinsTip(), /*check_sigs=*/true, state)) { return MempoolAcceptResult::Failure(state); } @@ -2657,11 +2656,13 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state, bool fDIP0001Active_context = DeploymentActiveAt(*pindex, params.GetConsensus(), Consensus::DEPLOYMENT_DIP0001); const CAmount blockSubsidy = GetBlockSubsidy(pindex, params.GetConsensus()); - const bool is_v24_active{DeploymentActiveAfter(pindex->pprev, m_chainman, Consensus::DEPLOYMENT_V24)}; + const SpecialTxRules special_tx_rules{GetSpecialTxRules(pindex->pprev, m_chainman)}; // MUST process special txes before updating UTXO to ensure consistency between mempool and block processing MNListUpdates mnlist_updates; - if (!m_chain_helper->special_tx->ProcessSpecialTxsInBlock(*this, m_chain, block, pindex, is_v24_active, view, blockSubsidy, fJustCheck, fScriptChecks, state, mnlist_updates)) { + if (!m_chain_helper->special_tx->ProcessSpecialTxsInBlock(*this, m_chain, block, pindex, special_tx_rules, view, + blockSubsidy, fJustCheck, fScriptChecks, state, + mnlist_updates)) { LogError("ConnectBlock(DASH): ProcessSpecialTxsInBlock for block %s failed with %s\n", pindex->GetBlockHash().ToString(), state.ToString()); return false; @@ -2839,7 +2840,7 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state, const SuperBlockCheckType check_superblock = !m_chain_helper->IsSuperblockValidationRequired(pindex) ? SuperBlockCheckType::NoCheck - : is_v24_active ? SuperBlockCheckType::DisallowDuplicates : SuperBlockCheckType::AllowDuplicates; + : special_tx_rules.v24 ? SuperBlockCheckType::DisallowDuplicates : SuperBlockCheckType::AllowDuplicates; if (!m_chain_helper->mn_payments->IsBlockValueValid(m_chain, block, pindex->pprev, blockSubsidy + feeReward, strError, check_superblock)) { @@ -2856,7 +2857,8 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state, Ticks(time_value_valid) / num_blocks_total); const MnRewardEra mn_reward_era{GetMnRewardEraAfter(pindex->pprev, m_chainman)}; - if (!m_chain_helper->mn_payments->IsBlockPayeeValid(m_chain, *block.vtx[0], pindex->pprev, blockSubsidy, feeReward, mn_reward_era, is_v24_active, check_superblock)) { + if (!m_chain_helper->mn_payments->IsBlockPayeeValid(m_chain, *block.vtx[0], pindex->pprev, blockSubsidy, feeReward, + mn_reward_era, special_tx_rules.v24, check_superblock)) { // NOTE: Do not punish, the node might be missing governance data LogPrintf("ERROR: ConnectBlock(DASH): couldn't find masternode or superblock payments\n"); return state.Invalid(BlockValidationResult::BLOCK_RESULT_UNSET, "bad-cb-payee"); @@ -5191,9 +5193,11 @@ bool Chainstate::RollforwardBlock(const CBlockIndex* pindex, CCoinsViewCache& in // MUST process special txes before updating UTXO to ensure consistency between mempool and block processing BlockValidationState state; const CAmount blockSubsidy = GetBlockSubsidy(pindex, m_chainman.GetConsensus()); - const bool is_v24_active{DeploymentActiveAfter(pindex->pprev, m_chainman, Consensus::DEPLOYMENT_V24)}; MNListUpdates mnlist_updates; - if (!m_chain_helper->special_tx->ProcessSpecialTxsInBlock(*this, m_chain, block, pindex, is_v24_active, inputs, blockSubsidy, /*fJustCheck=*/false, /*fCheckCbTxMerkleRoots=*/false, state, mnlist_updates)) { + if (!m_chain_helper->special_tx->ProcessSpecialTxsInBlock(*this, m_chain, block, pindex, + GetSpecialTxRules(pindex->pprev, m_chainman), inputs, + blockSubsidy, /*fJustCheck=*/false, + /*fCheckCbTxMerkleRoots=*/false, state, mnlist_updates)) { LogError("RollforwardBlock(DASH): ProcessSpecialTxsInBlock for block %s failed with %s\n", pindex->GetBlockHash().ToString(), state.ToString()); return false; @@ -6790,6 +6794,14 @@ bool IsBIP30Unspendable(const CBlockIndex& block_index) return false; } +SpecialTxRules GetSpecialTxRules(const CBlockIndex* pindexPrev, const ChainstateManager& chainman) +{ + return SpecialTxRules{ + .v24 = DeploymentActiveAfter(pindexPrev, chainman, Consensus::DEPLOYMENT_V24), + .evo_shares = DeploymentActiveAfter(pindexPrev, chainman, Consensus::DEPLOYMENT_EVO_SHARES), + }; +} + [[nodiscard]] uint16_t DeploymentToProtxVersion(gsl::not_null pindexPrev, const ChainstateManager& chainman, std::optional is_basic_override) { diff --git a/src/validation.h b/src/validation.h index e0c4087df5f0..2b6e8e6ce9f9 100644 --- a/src/validation.h +++ b/src/validation.h @@ -61,6 +61,7 @@ struct PrecomputedTransactionData; struct ChainTxData; class DisconnectedBlockTransactions; struct LockPoints; +struct SpecialTxRules; struct AssumeutxoData; namespace Consensus { struct Params; @@ -1273,6 +1274,9 @@ bool DeploymentEnabled(const ChainstateManager& chainman, DEP dep) return DeploymentEnabled(chainman.GetConsensus(), dep); } +/** The special transaction rules in force for the block after pindexPrev */ +SpecialTxRules GetSpecialTxRules(const CBlockIndex* pindexPrev, const ChainstateManager& chainman); + /** 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. diff --git a/test/functional/feature_evo_shares_activation.py b/test/functional/feature_evo_shares_activation.py new file mode 100755 index 000000000000..c44ffa5e4702 --- /dev/null +++ b/test/functional/feature_evo_shares_activation.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 The Dash Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test the evo_shares deployment. + +v24 follows evo_shares without signing it, so on a real network it stays inactive until a later +release signs it. Until then an EvoNode keeps a single owner payout. The test first runs the +deployment as shipped (masternode-activated), then lets miners activate it (-vbparams with EHF +disabled) to check the restriction lifts. +""" + +from test_framework.test_framework import DashTestFramework, MasternodeInfo, p2p_port +from test_framework.util import assert_equal, get_bip9_details, softfork_active + +EVO_SHARES_BIT = 14 +TESTDUMMY_BIT = 28 +V24_MIN_ACTIVATION_HEIGHT = 200 + + +def payout_address_rewards(payouts): + return [{"address": p["address"], "reward": p["reward"]} for p in payouts] + + +class EvoSharesActivationTest(DashTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + + def set_test_params(self): + extra_args = [[ + f"-vbparams=v24:{self.mocktime}:999999999999:{V24_MIN_ACTIVATION_HEIGHT}:10:8:6:5:0", + # A deployment masternodes do sign, to show that the evo_shares check below is not vacuous + "-vbparams=testdummy:0:999999999999:0:4:4:4:5:1", + "-persistmempool=0", + ] for _ in range(2)] + # One masternode is enough: an MNHF signal only needs a recovered signature of any quorum + self.set_dash_test_params(2, 1, extra_args=extra_args) + self.set_dash_llmq_test_params(1, 1) + + def skip_test_if_missing_module(self): + self.skip_if_no_wallet() + + def mnhf_signals_in_mempool(self, node, version_bit): + return [txid for txid in node.getrawmempool() + if node.getrawtransaction(txid, True).get("mnhfTx", {}).get("signal", {}).get("versionBit") == version_bit] + + def test_not_signed(self): + self.log.info("Masternodes sign other deployments but not evo_shares") + node = self.nodes[0] + mn_node = self.mninfo[0].get_node(self) + self.mine_quorum_single_member() + with mn_node.assert_debug_log(expected_msgs=[f"bit={TESTDUMMY_BIT} at height"], + unexpected_msgs=[f"bit={EVO_SHARES_BIT} at height"]): + self.generate(node, 1, sync_fun=self.sync_blocks) + self.wait_until(lambda: len(self.mnhf_signals_in_mempool(mn_node, TESTDUMMY_BIT)) == 1) + assert_equal(self.mnhf_signals_in_mempool(mn_node, EVO_SHARES_BIT), []) + self.generate(mn_node, 1, sync_fun=self.sync_blocks) + assert_equal(get_bip9_details(node, "evo_shares")["status"], "defined") + + @staticmethod + def evonode_platform_args(evo): + return { + "platform_node_id": "%040x" % evo.nodePort, + "addrs_platform_p2p": [f"127.0.0.1:{evo.nodePort + 1000}"], + "addrs_platform_https": [f"127.0.0.1:{evo.nodePort + 2000}"], + } + + def fund_evonode(self, node, port): + evo = MasternodeInfo(evo=True, legacy=False) + evo.generate_addresses(node) + evo.nodePort = port + collateral_txid = node.sendmany("", {evo.collateral_address: evo.get_collateral_value(), evo.fundsAddr: 1}) + self.bury_tx(node, collateral_txid) + evo.collateral_txid = collateral_txid + evo.collateral_vout = evo.get_collateral_vout(node, collateral_txid) + return evo + + @staticmethod + def two_payouts(node): + return [{"address": node.getnewaddress(), "reward": 6000}, {"address": node.getnewaddress(), "reward": 4000}] + + def test_single_payout(self): + self.log.info("An EvoNode keeps a single owner payout until evo_shares is active") + node = self.nodes[0] + while not softfork_active(node, "v24"): + self.generate(node, 10, sync_fun=self.sync_blocks) + assert not softfork_active(node, "evo_shares") + evo = self.fund_evonode(node, p2p_port(10)) + platform = self.evonode_platform_args(evo) + evo.register(node, submit=True, payouts=self.two_payouts(node), **platform, + expected_assert_code=-1, expected_assert_msg="bad-protx-payouts-evo") + protx_hash = evo.register(node, submit=True, **platform) + self.bury_tx(node, protx_hash) + evo.set_params(proTxHash=protx_hash) + + node.sendtoaddress(evo.fundsAddr, 1) + evo.update_registrar(node, submit=True, payouts=self.two_payouts(node), fundsAddr=evo.fundsAddr, + expected_assert_code=-1, expected_assert_msg="bad-protx-payouts-evo") + # Rotating the operator key does not open a way around it + evo.update_registrar(node, submit=True, pubKeyOperator=node.bls("generate")["public"], + payouts=self.two_payouts(node), fundsAddr=evo.fundsAddr, + expected_assert_code=-1, expected_assert_msg="bad-protx-payouts-evo") + assert_equal(len(node.protx("info", protx_hash)["state"]["payouts"]), 1) + return evo + + def activate_evo_shares_by_miners(self): + self.log.info("Let miners activate evo_shares") + for i in range(self.num_nodes): + self.extra_args[i].append("-vbparams=evo_shares:0:999999999999:0:10:8:6:5:0") + self.restart_node(0) + mn = self.mninfo[0] + self.stop_node(mn.nodeIdx) + self.start_masternode(mn) + self.connect_nodes(mn.nodeIdx, 0) + node = self.nodes[0] + while not softfork_active(node, "evo_shares"): + self.generate(node, 10, sync_fun=self.sync_blocks) + + def test_multiple_payouts(self, evo): + self.log.info("EvoNodes can have multiple owner payouts once evo_shares is active") + node = self.nodes[0] + payouts = self.two_payouts(node) + update_hash = evo.update_registrar(node, submit=True, payouts=payouts, fundsAddr=evo.fundsAddr) + self.bury_tx(node, update_hash) + assert_equal(payout_address_rewards(node.protx("info", evo.proTxHash)["state"]["payouts"]), payouts) + + evo2 = self.fund_evonode(node, p2p_port(11)) + payouts = self.two_payouts(node) + protx_hash = evo2.register(node, submit=True, payouts=payouts, **self.evonode_platform_args(evo2)) + self.bury_tx(node, protx_hash) + assert_equal(payout_address_rewards(node.protx("info", protx_hash)["state"]["payouts"]), payouts) + + def run_test(self): + self.nodes[0].sporkupdate("SPORK_17_QUORUM_DKG_ENABLED", 0) + self.wait_for_sporks_same() + self.test_not_signed() + evo = self.test_single_payout() + self.activate_evo_shares_by_miners() + self.test_multiple_payouts(evo) + + +if __name__ == '__main__': + EvoSharesActivationTest().main() diff --git a/test/functional/feature_masternode_shares.py b/test/functional/feature_masternode_shares.py index 4acfdd83c1fa..b4855a999899 100755 --- a/test/functional/feature_masternode_shares.py +++ b/test/functional/feature_masternode_shares.py @@ -18,6 +18,7 @@ assert_equal, assert_greater_than, assert_raises_rpc_error, + get_bip9_details, softfork_active, ) from test_framework.wallet_util import get_generate_key @@ -25,7 +26,11 @@ # Keep the earliest activation height above the ~119 blocks the framework setup mines, so # run_test starts with v24 locked in but not yet active and can exercise pre-activation rules V24_MIN_ACTIVATION_HEIGHT = 250 +# Above the ~1300 blocks the regular shared masternode checks mine, which run before evo_shares +# activates +EVO_SHARES_MIN_ACTIVATION_HEIGHT = 2000 COLLATERAL = 1000 * COIN +EVO_COLLATERAL = 4000 * COIN SHARED_COLLATERAL_SCRIPT = "04445348437551" # Must comfortably exceed the blocks mined between the first registration and its early-period # dissolution checks (~16 worst case), so the masternode is still inside the early period there @@ -42,25 +47,26 @@ def add_options(self, parser): self.add_wallet_options(parser) def set_test_params(self): + # evo_shares is activated by miners here; as shipped, masternodes do not sign it self.set_dash_test_params(2, 0, extra_args=[[ f"-vbparams=v24:{self.mocktime}:999999999999:{V24_MIN_ACTIVATION_HEIGHT}:10:8:6:5:0", + f"-vbparams=evo_shares:{self.mocktime}:999999999999:{EVO_SHARES_MIN_ACTIVATION_HEIGHT}:10:8:6:5:0", ]] * 2) - def activate_v24(self): - while not softfork_active(self.nodes[0], "v24"): + def activate(self, name): + while not softfork_active(self.nodes[0], name): self.bump_mocktime(50) self.generate(self.nodes[0], 50, sync_fun=self.no_op) - assert softfork_active(self.nodes[0], "v24") - def build_funding_tx(self, node): + def build_funding_tx(self, node, collateral=COLLATERAL): """Returns hex of a transaction with enough inputs to fund the collateral plus fee and a change output, but without the collateral output itself (shared_register_prepare appends it).""" dummy = node.getnewaddress() - raw = node.createrawtransaction([], {dummy: 1000}) + raw = node.createrawtransaction([], {dummy: collateral // COIN}) funded = node.fundrawtransaction(raw, {"feeRate": 0.00010000})["hex"] tx = tx_from_hex(funded) - vout = [out for out in tx.vout if out.nValue != COLLATERAL] + vout = [out for out in tx.vout if out.nValue != collateral] assert_equal(len(vout), len(tx.vout) - 1) tx.vout = vout return tx.serialize().hex() @@ -593,7 +599,7 @@ def run_test(self): pre_tmpl_txid = node.decoderawtransaction(pre_tmpl_signed["hex"])["txid"] self.generateblock(node, pre_miner, [pre_tmpl_signed["hex"]], sync_fun=self.no_op) - self.activate_v24() + self.activate("v24") self.log.info("A coinbase cannot create a shared collateral template output") assert_raises_rpc_error(-1, "bad-shared-collateral-create", self.generateblock, node, @@ -1222,6 +1228,90 @@ def submit_with_sigs(sigs): assert_greater_than(node.getreceivedbyaddress(addr, 1), Decimal(0)) self.test_separate_participant_wallets() + self.test_evonode_shared_registration() + + def test_evonode_shared_registration(self): + self.log.info("An EvoNode cannot be prepared for shared collateral before evo_shares activation") + node = self.nodes[0] + wallet = node.get_wallet_rpc(self.default_wallet_name) + assert not softfork_active(node, "evo_shares") + port = p2p_port(9) + platform_node_id = "%040x" % port + shares = [ + {"amount": 3000 * COIN, "refundAddress": wallet.getnewaddress(), "ownerAddress": wallet.getnewaddress()}, + {"amount": 1000 * COIN, "refundAddress": wallet.getnewaddress(), "ownerAddress": wallet.getnewaddress()}, + ] + # Funded before activation so that its inputs are mature below the activation height too + funding_hex = self.build_funding_tx(wallet, EVO_COLLATERAL) + operator = node.bls("generate") + args = [funding_hex, shares, f"127.0.0.1:{port}", operator["public"], wallet.getnewaddress(), 0, + EARLY_PERIOD_BLOCKS, EARLY_PENALTY, platform_node_id, [f"127.0.0.1:{port + 1000}"], + [f"127.0.0.1:{port + 2000}"]] + assert_raises_rpc_error(-8, "requires the evo_shares deployment to be active", wallet.protx, + "shared_register_prepare_evo", *args) + + self.activate("evo_shares") + prepared = wallet.protx("shared_register_prepare_evo", *args) + assert_equal(prepared["terms"]["platformNodeID"], platform_node_id) + signatures = wallet.protx("shared_sign", prepared["tx"])["signatures"] + signed = wallet.signrawtransactionwithwallet(wallet.protx("shared_combine", prepared["tx"], signatures)) + assert_equal(signed["complete"], True) + + self.log.info("A shared EvoNode registration is invalid in a block before evo_shares activation") + last_inactive = node.getblockhash(get_bip9_details(node, "evo_shares")["since"] - 1) + node.invalidateblock(last_inactive) + assert not softfork_active(node, "evo_shares") + self.assert_rejected_transaction(node, tx_from_hex(signed["hex"]), "bad-protx-shares-evo") + node.reconsiderblock(last_inactive) + + self.log.info("An EvoNode registers with shared collateral once evo_shares is active") + protx_hash = node.sendrawtransaction(signed["hex"]) + self.bump_mocktime(10 * 60 + 1) + self.generate(node, 1, sync_fun=self.no_op) + info = node.protx("info", protx_hash) + assert_equal(info["type"], "Evo") + assert_equal(info["state"]["platformNodeID"], platform_node_id) + assert_equal([share["amount"] for share in info["state"]["shares"]], [share["amount"] for share in shares]) + + self.log.info("A shared EvoNode's owner reward is split by share amounts") + payees = self.owner_gbt_payees(node) + assert_equal([p["payee"] for p in payees], [share["refundAddress"] for share in shares]) + owner_total = sum(p["amount"] for p in payees) + assert_equal(payees[0]["amount"], owner_total * shares[0]["amount"] // EVO_COLLATERAL) + assert_equal(payees[1]["amount"], owner_total - payees[0]["amount"]) + self.generate(node, 1, sync_fun=self.no_op) + + self.log.info("A shared EvoNode updates its Platform service, a share reward and its voting key") + fee_addresses = [wallet.getnewaddress() for _ in range(3)] + wallet.sendmany("", {address: 1 for address in fee_addresses}) + self.bump_mocktime(10 * 60 + 1) + self.generate(node, 1, sync_fun=self.no_op) + new_platform_node_id = "%040x" % (port + 1) + wallet.protx("update_service_evo", protx_hash, [f"127.0.0.1:{port}"], operator["secret"], new_platform_node_id, + [f"127.0.0.1:{port + 3000}"], [f"127.0.0.1:{port + 4000}"], "", fee_addresses[0]) + reward = wallet.getnewaddress() + wallet.protx("shared_update_share", protx_hash, 0, reward, fee_addresses[1]) + voting = wallet.getnewaddress() + prepared = wallet.protx("shared_update_registrar_prepare", protx_hash, "", voting, fee_addresses[2]) + wallet.protx("shared_combine", prepared["tx"], wallet.protx("shared_sign", prepared["tx"])["signatures"], True) + self.bump_mocktime(10 * 60 + 1) + self.generate(node, 1, sync_fun=self.no_op) + state = node.protx("info", protx_hash)["state"] + assert_equal(state["platformNodeID"], new_platform_node_id) + assert_equal(state["shares"][0]["rewardAddress"], reward) + assert_equal(state["votingAddress"], voting) + + self.log.info("A shared EvoNode dissolves and refunds its collateral to every share") + prepared = wallet.protx("shared_dissolve_prepare", protx_hash, 1, DISSOLVE_FEE) + dissolution = wallet.protx("shared_combine", prepared["tx"], wallet.protx("shared_sign", prepared["tx"])["signatures"]) + refunds = {out["scriptPubKey"]["address"]: out["value"] for out in node.decoderawtransaction(dissolution)["vout"]} + assert_equal(refunds, {shares[0]["refundAddress"]: Decimal(shares[0]["amount"]) / COIN, + shares[1]["refundAddress"]: Decimal(shares[1]["amount"] - DISSOLVE_FEE) / COIN}) + dissolve_txid = node.sendrawtransaction(dissolution) + self.bump_mocktime(10 * 60 + 1) + block_hash = self.generate(node, 1, sync_fun=self.no_op)[0] + assert dissolve_txid in node.getblock(block_hash)["tx"] + assert_equal(node.masternodelist(), {}) if __name__ == '__main__': diff --git a/test/functional/rpc_blockchain.py b/test/functional/rpc_blockchain.py index 4a7472db76a3..bf8bfa0500d2 100755 --- a/test/functional/rpc_blockchain.py +++ b/test/functional/rpc_blockchain.py @@ -227,6 +227,19 @@ def check_signalling_deploymentinfo_result(self, gdi_result, height, blockhash, }, 'active': False }, + 'evo_shares': { + 'type': 'bip9', + 'bip9': { + 'start_time': 0, + 'timeout': 9223372036854775807, # "evo_shares" does not have a timeout so is set to the max int64 value + 'min_activation_height': 0, + 'since': 0, + 'status': 'defined', + 'status_next': 'defined', + 'ehf': True, + }, + 'active': False + }, 'testdummy': { 'type': 'bip9', 'bip9': { diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 0b5e675ae1c8..4d6bee430d26 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -131,6 +131,7 @@ 'feature_llmq_is_cl_conflicts.py', # NOTE: needs dash_hash to pass 'feature_dip4_coinbasemerkleroots.py', # NOTE: needs dash_hash to pass 'feature_mnehf.py', # NOTE: needs dash_hash to pass + 'feature_evo_shares_activation.py', # NOTE: needs dash_hash to pass 'feature_masternode_params.py', # NOTE: needs dash_hash to pass 'feature_masternode_params_devnet.py', # NOTE: needs dash_hash to pass 'feature_governance.py --descriptors',