diff --git a/contrib/completions/fish/dash-cli.fish b/contrib/completions/fish/dash-cli.fish index 9a059d5f4ed4..8353506726a9 100644 --- a/contrib/completions/fish/dash-cli.fish +++ b/contrib/completions/fish/dash-cli.fish @@ -9,14 +9,20 @@ function __fish_dash_cli_get_commands_helper return end - # Strip help cmd from token to avoid duplication errors - set --local cmd (string match --invert --regex -- '^help$' $cmd) - # Strip -stdin* options to avoid waiting for input while we fetch completions - # TODO: this appears to be broken when run as tab completion (requires ctrl+c to exit) - set --local cmd (string match --invert --regex -- '^-stdin.*$' $cmd) + # Only keep the connection options given before the RPC command. Never pass + # on the typed command or its arguments: they would be executed on every Tab + # (e.g. `sendtoaddress
` would send funds). + # -rpcwait* is dropped so Tab doesn't hang while the node is down. + set --local base $cmd[1] + for arg in $cmd[2..-1] + string match --quiet --regex -- '^-' $arg; or break + if string match --quiet --regex -- '^--?(conf|datadir|testnet|regtest|devnet|chain|rpc(?!wait))' $arg + set --append base $arg + end + end # Match, format and return commands - for command in ($cmd help 2>&1 | string match --invert -r '^\=\=.*' | string match --invert -r '^\\s*$') + for command in ($base help 2>&1 | string match --invert -r '^\=\=.*' | string match --invert -r '^\\s*$') echo $command end end @@ -60,9 +66,9 @@ function __fish_dash_cli_get_options set --local options if set -q _flag_nofiles - set --append options ($cmd -help 2>&1 | string match -r '^ -.*' | string replace -r ' -' '-' | string replace -r '=.*' '=' | string match --invert -r '^.*=$') + set --append options ($cmd[1] -help 2>&1 | string match -r '^ -.*' | string replace -r ' -' '-' | string replace -r '=.*' '=' | string match --invert -r '^.*=$') else - set --append options ($cmd -help 2>&1 | string match -r '^ -.*' | string replace -r ' -' '-' | string replace -r '=.*' '=' | string match -r '^.*=$') + set --append options ($cmd[1] -help 2>&1 | string match -r '^ -.*' | string replace -r ' -' '-' | string replace -r '=.*' '=' | string match -r '^.*=$') end for option in $options diff --git a/doc/release-notes-7300.md b/doc/release-notes-7300.md index a9d63a16eaf2..bbae7e647268 100644 --- a/doc/release-notes-7300.md +++ b/doc/release-notes-7300.md @@ -3,6 +3,14 @@ P2P and network changes * Startup now fails if any configured `-bind`, `-whitebind` or the implicit Tor onion-service bind (`127.0.0.1:9996` by default) cannot be set up. Dash Core - v23 and earlier started as long as at least one bind succeeded. Use - `-bind=:=onion` to move the onion bind, or `-listenonion=0` to - drop it, if the default address is not available on your system. + v23 and earlier started as long as at least one bind succeeded. + +* Nodes configured with `-bind` but no specific `-bind==onion` now + refuse to start when `-listenonion` is enabled. This includes nodes without + Tor configured, since `-listenonion` is enabled by default when listening. + Shared binds cannot distinguish Tor-forwarded connections from direct + connections, which can grant Tor peers unintended IP-based whitelist + permissions. Nodes without `-bind`, including those using only `-whitebind`, + continue to get the default onion target. Users should add a specific + `-bind==onion` to accept incoming Tor connections, or set + `-listenonion=0` to disable automatic onion service creation. diff --git a/doc/release-notes-7779.md b/doc/release-notes-7779.md new file mode 100644 index 000000000000..6358edcfd0d5 --- /dev/null +++ b/doc/release-notes-7779.md @@ -0,0 +1,6 @@ +Updated RPCs +------------ + +- `protx listdiff` reports the complete `addresses` object, `platform_p2p` and `platform_https` included, + when the core P2P address of an EvoNode on legacy (pre-extended) addresses changes, so its Platform + ports can be followed without `-deprecatedrpc=service`. (#7779) 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/doc/tor.md b/doc/tor.md index 3cd32aee785f..d617a58e510d 100644 --- a/doc/tor.md +++ b/doc/tor.md @@ -83,6 +83,9 @@ it requires a Tor connection to work. It can be explicitly disabled with `-listenonion=0`. If it is not disabled, it can be configured using the `-torcontrol` and `-torpassword` settings. +When enabled with an explicit `-bind`, the automatic service requires a non-wildcard onion bind such as `-bind=127.0.0.1:=onion`. +Without `-bind`, Dash Core adds the default onion bind. + To see verbose Tor information in the dashd debug log, pass `-debug=tor`. ### Control Port 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/coinjoin/common.h b/src/coinjoin/common.h index 3a3b3177bc90..8765c4633f84 100644 --- a/src/coinjoin/common.h +++ b/src/coinjoin/common.h @@ -120,15 +120,23 @@ constexpr CAmount GetMaxCollateralAmount() { return GetCollateralAmount() * 4; } // Promotion/demotion constants (post-V24 feature) constexpr int PROMOTION_RATIO = 10; // 10 smaller denomination coins = 1 larger denomination coin constexpr int GAP_DIVISOR = 5; // Deficit gap required to trigger promotion/demotion, as 1/N of the goal +/** + * Smallest gap that stops a conversion from making the opposite conversion worthwhile. A + * conversion needs the receiving deficit to exceed the other by more than the gap, and it moves + * the two deficits by at most PROMOTION_RATIO and 1, so reversing it right away would need + * 2 * gap + 1 <= PROMOTION_RATIO. Any gap with 2 * gap + 1 > PROMOTION_RATIO rules that out. + */ +constexpr int MIN_GAP_THRESHOLD = (PROMOTION_RATIO + 1) / 2; /** * How far behind the other denomination a denomination has to be before converting is worth it; * the gap is what keeps promotion and demotion from oscillating. It is a fraction of the goal * rather than a constant because the largest gap two denominations can show is the goal itself: * a constant of 10 was unsatisfiable at MIN_COINJOIN_DENOMS_GOAL (also 10) and silently - * disabled the feature there. At the default goal of 50 this still yields 10. + * disabled the feature there. At the default goal of 50 this still yields 10; below a goal of 25 + * it is floored at MIN_GAP_THRESHOLD. */ -constexpr int GetGapThreshold(int nGoal) { return std::max(nGoal / GAP_DIVISOR, 1); } +constexpr int GetGapThreshold(int nGoal) { return std::max(nGoal / GAP_DIVISOR, MIN_GAP_THRESHOLD); } /** * Which side(s) of the session denomination a participant occupies. A standard entry mixes at 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/governance/governance.cpp b/src/governance/governance.cpp index bc15e1424c00..6e34c5c56e95 100644 --- a/src/governance/governance.cpp +++ b/src/governance/governance.cpp @@ -427,13 +427,17 @@ void CGovernanceManager::CheckAndRemove() // IF DELETE=TRUE, THEN CLEAN THE MESS UP! - const auto nTimeSinceDeletion = nNow - std::chrono::seconds{pObj->GetDeletionTime()}; + // Read the flags before the deletion time: another thread can mark the object for deletion + // at any point, and a flag read after a zero deletion time would skip GOVERNANCE_DELETION_DELAY. + const bool fCachedDelete{pObj->IsSetCachedDelete()}; + const bool fExpired{pObj->IsSetExpired()}; + const int64_t nDeletionTime{pObj->GetDeletionTime()}; + const auto nTimeSinceDeletion = nNow - std::chrono::seconds{nDeletionTime}; LogPrint(BCLog::GOBJECT, "CGovernanceManager::UpdateCachesAndClean -- Checking object for deletion: %s, deletion time = %d, time since deletion = %d, delete flag = %d, expired flag = %d\n", - strHash, pObj->GetDeletionTime(), nTimeSinceDeletion.count(), pObj->IsSetCachedDelete(), pObj->IsSetExpired()); + strHash, nDeletionTime, nTimeSinceDeletion.count(), fCachedDelete, fExpired); - if ((pObj->IsSetCachedDelete() || pObj->IsSetExpired()) && - (nTimeSinceDeletion >= GOVERNANCE_DELETION_DELAY)) { + if ((fCachedDelete || fExpired) && (nTimeSinceDeletion >= GOVERNANCE_DELETION_DELAY)) { LogPrint(BCLog::GOBJECT, "CGovernanceManager::UpdateCachesAndClean -- erase obj %s\n", nHash.ToString()); m_mn_metaman.RemoveGovernanceObject(pObj->GetHash()); diff --git a/src/governance/object.cpp b/src/governance/object.cpp index 240e1378ebfd..9a5eb49880fb 100644 --- a/src/governance/object.cpp +++ b/src/governance/object.cpp @@ -356,19 +356,20 @@ CGovernanceObject::CGovernanceObject(const uint256& nHashParentIn, int nRevision CGovernanceObject::CGovernanceObject(const CGovernanceObject& other) : cs(), m_obj{other.m_obj}, - nDeletionTime(other.nDeletionTime), fCachedLocalValidity(other.fCachedLocalValidity), strLocalValidityError(other.strLocalValidityError), - fCachedFunding(other.fCachedFunding), - fCachedValid(other.fCachedValid), - fCachedDelete(other.fCachedDelete), - fCachedEndorsed(other.fCachedEndorsed), - fDirtyCache(other.fDirtyCache), - fExpired(other.fExpired), - fUnparsable(other.fUnparsable), - mapCurrentMNVotes(other.mapCurrentMNVotes), - fileVotes(other.fileVotes) + fUnparsable(other.fUnparsable) { + LOCK(other.cs); + nDeletionTime = other.nDeletionTime; + fCachedFunding = other.fCachedFunding; + fCachedValid = other.fCachedValid; + fCachedDelete = other.fCachedDelete; + fCachedEndorsed = other.fCachedEndorsed; + fDirtyCache = other.fDirtyCache; + fExpired = other.fExpired; + mapCurrentMNVotes = other.mapCurrentMNVotes; + fileVotes = other.fileVotes; } bool CGovernanceObject::ProcessVote(CMasternodeMetaMan& mn_metaman, bool fRateChecksEnabled, @@ -614,10 +615,11 @@ UniValue CGovernanceObject::GetStateJson(const ChainstateManager& chainman, cons std::string strError; ret.pushKV(local_valid_key, WITH_LOCK(::cs_main, return IsValidLocally(tip_mn_list, chainman, strError, /*fCheckCollateral=*/false))); ret.pushKV("IsValidReason", strError.c_str()); - ret.pushKV("fCachedValid", IsSetCachedValid()); - ret.pushKV("fCachedFunding", IsSetCachedFunding()); - ret.pushKV("fCachedDelete", IsSetCachedDelete()); - ret.pushKV("fCachedEndorsed", IsSetCachedEndorsed()); + LOCK(cs); + ret.pushKV("fCachedValid", fCachedValid); + ret.pushKV("fCachedFunding", fCachedFunding); + ret.pushKV("fCachedDelete", fCachedDelete); + ret.pushKV("fCachedEndorsed", fCachedEndorsed); return ret; } @@ -877,7 +879,7 @@ bool CGovernanceObject::IsCollateralValid(const ChainstateManager& chainman, std int CGovernanceObject::CountMatchingVotes(const CDeterministicMNList& tip_mn_list, vote_signal_enum_t eVoteSignalIn, vote_outcome_enum_t eVoteOutcomeIn) const { - LOCK(cs); + AssertLockHeld(cs); int nCount = 0; for (const auto& [outpoint, recVote] : mapCurrentMNVotes) { @@ -911,19 +913,19 @@ int CGovernanceObject::GetAbsoluteNoCount(const CDeterministicMNList& tip_mn_lis int CGovernanceObject::GetYesCount(const CDeterministicMNList& tip_mn_list, vote_signal_enum_t eVoteSignalIn) const { AssertLockNotHeld(cs); - return CountMatchingVotes(tip_mn_list, eVoteSignalIn, VOTE_OUTCOME_YES); + return WITH_LOCK(cs, return CountMatchingVotes(tip_mn_list, eVoteSignalIn, VOTE_OUTCOME_YES)); } int CGovernanceObject::GetNoCount(const CDeterministicMNList& tip_mn_list, vote_signal_enum_t eVoteSignalIn) const { AssertLockNotHeld(cs); - return CountMatchingVotes(tip_mn_list, eVoteSignalIn, VOTE_OUTCOME_NO); + return WITH_LOCK(cs, return CountMatchingVotes(tip_mn_list, eVoteSignalIn, VOTE_OUTCOME_NO)); } int CGovernanceObject::GetAbstainCount(const CDeterministicMNList& tip_mn_list, vote_signal_enum_t eVoteSignalIn) const { AssertLockNotHeld(cs); - return CountMatchingVotes(tip_mn_list, eVoteSignalIn, VOTE_OUTCOME_ABSTAIN); + return WITH_LOCK(cs, return CountMatchingVotes(tip_mn_list, eVoteSignalIn, VOTE_OUTCOME_ABSTAIN)); } CGovernanceObject::UniqueVoterCount CGovernanceObject::GetUniqueVoterCount(const CDeterministicMNList& tip_mn_list, vote_signal_enum_t eVoteSignalIn) const @@ -973,27 +975,25 @@ void CGovernanceObject::UpdateSentinelVariables(const CDeterministicMNList& tip_ int nAbsVoteReq = std::max(Params().GetConsensus().nGovernanceMinQuorum, nWeightedMnCount / 10); int nAbsDeleteReq = std::max(Params().GetConsensus().nGovernanceMinQuorum, (2 * nWeightedMnCount) / 3); - // SET SENTINEL FLAGS TO FALSE - - fCachedFunding = false; - fCachedValid = true; //default to valid - fCachedEndorsed = false; - fDirtyCache = false; + // COUNT VOTES AND SET ALL SENTINEL FLAGS IN ONE CRITICAL SECTION - // SET SENTINEL FLAGS TO TRUE IF MINIMUM SUPPORT LEVELS ARE REACHED - // ARE ANY OF THESE FLAGS CURRENTLY ACTIVATED? + LOCK(cs); + const auto absolute_yes_count = [&](vote_signal_enum_t signal) EXCLUSIVE_LOCKS_REQUIRED(cs) { + return CountMatchingVotes(tip_mn_list, signal, VOTE_OUTCOME_YES) - + CountMatchingVotes(tip_mn_list, signal, VOTE_OUTCOME_NO); + }; + const auto absolute_no_count = [&](vote_signal_enum_t signal) EXCLUSIVE_LOCKS_REQUIRED(cs) { + return CountMatchingVotes(tip_mn_list, signal, VOTE_OUTCOME_NO) - + CountMatchingVotes(tip_mn_list, signal, VOTE_OUTCOME_YES); + }; - if (GetAbsoluteYesCount(tip_mn_list, VOTE_SIGNAL_FUNDING) >= nAbsVoteReq) fCachedFunding = true; - if ((GetAbsoluteYesCount(tip_mn_list, VOTE_SIGNAL_DELETE) >= nAbsDeleteReq) && !fCachedDelete) { - fCachedDelete = true; - LOCK(cs); - if (nDeletionTime == 0) { - nDeletionTime = GetTime().count(); - } + fDirtyCache = false; + fCachedFunding = absolute_yes_count(VOTE_SIGNAL_FUNDING) >= nAbsVoteReq; + if (absolute_yes_count(VOTE_SIGNAL_DELETE) >= nAbsDeleteReq) { + MarkForDeletion(GetTime().count()); } - if (GetAbsoluteYesCount(tip_mn_list, VOTE_SIGNAL_ENDORSED) >= nAbsVoteReq) fCachedEndorsed = true; - - if (GetAbsoluteNoCount(tip_mn_list, VOTE_SIGNAL_VALID) >= nAbsVoteReq) fCachedValid = false; + fCachedEndorsed = absolute_yes_count(VOTE_SIGNAL_ENDORSED) >= nAbsVoteReq; + fCachedValid = absolute_no_count(VOTE_SIGNAL_VALID) < nAbsVoteReq; } namespace governance { diff --git a/src/governance/object.h b/src/governance/object.h index 66faf23351fd..d967c42d04a8 100644 --- a/src/governance/object.h +++ b/src/governance/object.h @@ -157,21 +157,21 @@ class CGovernanceObject // VARIOUS FLAGS FOR OBJECT / SET VIA MASTERNODE VOTING /// true == minimum network support has been reached for this object to be funded (doesn't mean it will for sure though) - bool fCachedFunding{false}; + bool fCachedFunding GUARDED_BY(cs){false}; /// true == minimum network has been reached flagging this object as a valid and understood governance object (e.g, the serialized data is correct format, etc) - bool fCachedValid{true}; + bool fCachedValid GUARDED_BY(cs){true}; /// true == minimum network support has been reached saying this object should be deleted from the system entirely - bool fCachedDelete{false}; + bool fCachedDelete GUARDED_BY(cs){false}; /** true == minimum network support has been reached flagging this object as endorsed by an elected representative body * (e.g. business review board / technical review board /etc) */ - bool fCachedEndorsed{false}; + bool fCachedEndorsed GUARDED_BY(cs){false}; /// object was updated and cached values should be updated soon - bool fDirtyCache{true}; + bool fDirtyCache GUARDED_BY(cs){true}; /// Object is no longer of interest bool fExpired GUARDED_BY(cs){false}; @@ -192,11 +192,11 @@ class CGovernanceObject CGovernanceObject(deserialize_type, Stream& s) { s >> *this; } // Getters - bool IsSetCachedFunding() const { return fCachedFunding; } - bool IsSetCachedValid() const { return fCachedValid; } - bool IsSetCachedDelete() const { return fCachedDelete; } - bool IsSetCachedEndorsed() const { return fCachedEndorsed; } - bool IsSetDirtyCache() const { return fDirtyCache; } + bool IsSetCachedFunding() const EXCLUSIVE_LOCKS_REQUIRED(!cs) { return WITH_LOCK(cs, return fCachedFunding); } + bool IsSetCachedValid() const EXCLUSIVE_LOCKS_REQUIRED(!cs) { return WITH_LOCK(cs, return fCachedValid); } + bool IsSetCachedDelete() const EXCLUSIVE_LOCKS_REQUIRED(!cs) { return WITH_LOCK(cs, return fCachedDelete); } + bool IsSetCachedEndorsed() const EXCLUSIVE_LOCKS_REQUIRED(!cs) { return WITH_LOCK(cs, return fCachedEndorsed); } + bool IsSetDirtyCache() const EXCLUSIVE_LOCKS_REQUIRED(!cs) { return WITH_LOCK(cs, return fDirtyCache); } bool IsSetExpired() const EXCLUSIVE_LOCKS_REQUIRED(!cs) { return WITH_LOCK(cs, return fExpired); @@ -250,11 +250,8 @@ class CGovernanceObject void PrepareDeletion(int64_t nDeletionTime_) EXCLUSIVE_LOCKS_REQUIRED(!cs) { - fCachedDelete = true; LOCK(cs); - if (nDeletionTime == 0) { - nDeletionTime = nDeletionTime_; - } + MarkForDeletion(nDeletionTime_); } CAmount GetMinCollateralFee() const; @@ -266,9 +263,6 @@ class CGovernanceObject // GET VOTE COUNT FOR SIGNAL - int CountMatchingVotes(const CDeterministicMNList& tip_mn_list, vote_signal_enum_t eVoteSignalIn, vote_outcome_enum_t eVoteOutcomeIn) const - EXCLUSIVE_LOCKS_REQUIRED(!cs); - int GetAbsoluteYesCount(const CDeterministicMNList& tip_mn_list, vote_signal_enum_t eVoteSignalIn) const EXCLUSIVE_LOCKS_REQUIRED(!cs); int GetAbsoluteNoCount(const CDeterministicMNList& tip_mn_list, vote_signal_enum_t eVoteSignalIn) const @@ -350,6 +344,18 @@ class CGovernanceObject // Returns deleted vote hashes. std::set RemoveInvalidVotes(const CDeterministicMNList& tip_mn_list, const COutPoint& mnOutpoint) EXCLUSIVE_LOCKS_REQUIRED(!cs); + +private: + int CountMatchingVotes(const CDeterministicMNList& tip_mn_list, vote_signal_enum_t eVoteSignalIn, + vote_outcome_enum_t eVoteOutcomeIn) const EXCLUSIVE_LOCKS_REQUIRED(cs); + + void MarkForDeletion(int64_t nDeletionTime_) EXCLUSIVE_LOCKS_REQUIRED(cs) + { + fCachedDelete = true; + if (nDeletionTime == 0) { + nDeletionTime = nDeletionTime_; + } + } }; namespace governance { diff --git a/src/governance/votedb.cpp b/src/governance/votedb.cpp index 5c9b3ef99072..da0ee869e8cf 100644 --- a/src/governance/votedb.cpp +++ b/src/governance/votedb.cpp @@ -20,6 +20,15 @@ CGovernanceObjectVoteFile::CGovernanceObjectVoteFile(const CGovernanceObjectVote RebuildIndex(); } +CGovernanceObjectVoteFile& CGovernanceObjectVoteFile::operator=(const CGovernanceObjectVoteFile& other) +{ + if (this != &other) { + listVotes = vote_l_t{other.listVotes}; + RebuildIndex(); + } + return *this; +} + void CGovernanceObjectVoteFile::AddVote(const CGovernanceVote& vote) { uint256 nHash = vote.GetHash(); diff --git a/src/governance/votedb.h b/src/governance/votedb.h index 5032d6317001..15c39f5c7efd 100644 --- a/src/governance/votedb.h +++ b/src/governance/votedb.h @@ -44,6 +44,8 @@ class CGovernanceObjectVoteFile CGovernanceObjectVoteFile(const CGovernanceObjectVoteFile& other); + CGovernanceObjectVoteFile& operator=(const CGovernanceObjectVoteFile& other); + /** * Add a vote to the file */ diff --git a/src/init.cpp b/src/init.cpp index 4683a409b123..aa4d77b639ed 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -653,7 +653,8 @@ void SetupServerArgs(ArgsManager& argsman) argsman.AddArg("-addnode=", strprintf("Add a node to connect to and attempt to keep the connection open (see the addnode RPC help for more info). This option can be specified multiple times to add multiple nodes; connections are limited to %u at a time and are counted separately from the -maxconnections limit.", MAX_ADDNODE_CONNECTIONS), ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::CONNECTION); argsman.AddArg("-allowprivatenet", strprintf("Allow RFC1918 addresses to be relayed and connected to (default: %u)", DEFAULT_ALLOWPRIVATENET), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-bantime=", strprintf("Default duration (in seconds) of manually configured bans (default: %u)", DEFAULT_MISBEHAVING_BANTIME), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); - argsman.AddArg("-bind=[:][=onion]", strprintf("Bind to given address and always listen on it (default: 0.0.0.0). Use [host]:port notation for IPv6. Append =onion to tag any incoming connections to that address and port as incoming Tor connections (default: 127.0.0.1:%u=onion, testnet: 127.0.0.1:%u=onion, devnet: 127.0.0.1:%u=onion, regtest: 127.0.0.1:%u=onion)", defaultBaseParams->OnionServiceTargetPort(), testnetBaseParams->OnionServiceTargetPort(), devnetBaseParams->OnionServiceTargetPort(), regtestBaseParams->OnionServiceTargetPort()), ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::CONNECTION); + argsman.AddArg("-bind=[:][=onion]", strprintf("Bind to given address and always listen on it (default: 0.0.0.0). Use [host]:port notation for IPv6. Append =onion to tag any incoming connections to that address and port as incoming Tor connections (default: 127.0.0.1:%u=onion, testnet: 127.0.0.1:%u=onion, devnet: 127.0.0.1:%u=onion, regtest: 127.0.0.1:%u=onion). " + "The default onion bind is added only when no -bind is specified. When -listenonion is enabled and -bind is specified, a non-wildcard =onion bind is required, and wildcard =onion binds are rejected.", defaultBaseParams->OnionServiceTargetPort(), testnetBaseParams->OnionServiceTargetPort(), devnetBaseParams->OnionServiceTargetPort(), regtestBaseParams->OnionServiceTargetPort()), ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::CONNECTION); argsman.AddArg("-cjdnsreachable", "If set, then this host is configured for CJDNS (connecting to fc00::/8 addresses would lead us to the CJDNS network, see doc/cjdns.md) (default: 0)", ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-connect=", "Connect only to the specified node; -noconnect disables automatic connections (the rules for this peer are the same as for -addnode). This option can be specified multiple times to connect to multiple nodes.", ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::CONNECTION); argsman.AddArg("-discover", "Discover own IP addresses (default: 1 when listening and no -externalip or -proxy)", ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); @@ -1439,6 +1440,13 @@ bool AppInitParameterInteraction(const ArgsManager& args) } } + // Governance needs the transaction index to look up proposal collateral transactions + if (!args.GetBoolArg("-disablegovernance", !DEFAULT_GOVERNANCE_ENABLE) && + !args.GetBoolArg("-txindex", DEFAULT_TXINDEX) && chainparams.NetworkIDString() != CBaseChainParams::REGTEST) { + return InitError(_("Transaction index can't be disabled with governance validation enabled. Either start with " + "-disablegovernance command line switch or enable transaction index.")); + } + if (args.GetBoolArg("-disablegovernance", !DEFAULT_GOVERNANCE_ENABLE)) { InitWarning(_("You are starting with governance validation disabled.") + (fPruneMode ? @@ -2675,17 +2683,18 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) } } - CService onion_service_target; - if (!connOptions.onion_binds.empty()) { - onion_service_target = connOptions.onion_binds.front(); - } else if (!connOptions.vBinds.empty()) { - onion_service_target = connOptions.vBinds.front(); - } else { - onion_service_target = DefaultOnionServiceTarget(); - connOptions.onion_binds.push_back(onion_service_target); + if (connOptions.onion_binds.empty() && connOptions.vBinds.empty()) { + connOptions.onion_binds.push_back(DefaultOnionServiceTarget()); } if (args.GetBoolArg("-listenonion", DEFAULT_LISTEN_ONION)) { + if (connOptions.onion_binds.empty()) { + return InitError(_("The automatic Tor onion service requires a dedicated onion bind. Use a specific address such as -bind=127.0.0.1:=onion, or disable the service with -listenonion=0.")); + } + if (std::ranges::any_of(connOptions.onion_binds, [](auto& b) { return b.IsBindAny(); })) { + return InitError(_("The automatic Tor onion service cannot use a wildcard onion bind because the Tor daemon wouldn't be able to forward incoming connections to us. Use a specific address such as -bind=127.0.0.1:=onion, or disable the service with -listenonion=0.")); + } + const CService& onion_service_target{connOptions.onion_binds[0]}; if (connOptions.onion_binds.size() > 1) { InitWarning(strprintf(_("More than one onion bind address is provided. Using %s " "for the automatically created Tor onion service."), 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 a5e26f80f0fd..fc8563fdc197 100644 --- a/src/llmq/ehf_signals.cpp +++ b/src/llmq/ehf_signals.cpp @@ -33,16 +33,31 @@ CEHFSignalsHandler::~CEHFSignalsHandler() sigman.UnregisterRecoveredSigsListener(this); } +static bool IsSignalWindowOpen(const Consensus::BIP9Deployment& deployment, int64_t time_past) +{ + return deployment.nStartTime != Consensus::BIP9Deployment::NEVER_ACTIVE && deployment.nStartTime <= time_past && + time_past <= deployment.nTimeout; +} + void CEHFSignalsHandler::UpdatedBlockTip(const CBlockIndex* const pindexNew) { if (!DeploymentActiveAfter(pindexNew, Params().GetConsensus(), Consensus::DEPLOYMENT_V20)) return; const auto ehfSignals = m_chainman.ActiveChainstate().ChainHelper().ehf_manager->GetSignalsStage(pindexNew); - for (const auto& deployment : Params().GetConsensus().vDeployments) { + const int64_t time_past = pindexNew->GetMedianTimePast(); + 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; // Try to sign only activable deployments that haven't been mined yet - if (ehfSignals.find(deployment.bit) == ehfSignals.end() && Params().IsValidMNActivation(deployment.bit, pindexNew->GetMedianTimePast())) { + if (ehfSignals.find(deployment.bit) == ehfSignals.end() && Params().IsValidMNActivation(deployment.bit, time_past)) { trySignEHFSignal(deployment.bit, pindexNew); } } 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/node/sync_manager.cpp b/src/node/sync_manager.cpp index 4eb1598e284f..2f11b9f3cb31 100644 --- a/src/node/sync_manager.cpp +++ b/src/node/sync_manager.cpp @@ -83,7 +83,7 @@ int SyncManager::RequestGovernanceObjectVotes(const std::vector& vNodesC static std::map> mapAskedRecently GUARDED_BY(cs_recently); LOCK(cs_recently); - auto [vTriggerObjHashes, vOtherObjHashes] = m_gov_manager.FetchGovernanceObjectVotes(nMaxObjRequestsPerNode, nNow, + auto [vTriggerObjHashes, vOtherObjHashes] = m_gov_manager.FetchGovernanceObjectVotes(nPeersPerHashMax, nNow, mapAskedRecently); if (vTriggerObjHashes.empty() && vOtherObjHashes.empty()) return -2; diff --git a/src/psbt.cpp b/src/psbt.cpp index 2ba214b74d19..fdc2c4f149b7 100644 --- a/src/psbt.cpp +++ b/src/psbt.cpp @@ -181,10 +181,10 @@ void PSBTOutput::Merge(const PSBTOutput& output) if (redeem_script.empty() && !output.redeem_script.empty()) redeem_script = output.redeem_script; } -bool PSBTInputSignedAndVerified(const PartiallySignedTransaction psbt, unsigned int input_index, const PrecomputedTransactionData* txdata) +bool PSBTInputSignedAndVerified(const PartiallySignedTransaction& psbt, unsigned int input_index, const PrecomputedTransactionData* txdata) { CTxOut utxo; - assert(psbt.inputs.size() >= input_index); + assert(input_index < psbt.inputs.size()); const PSBTInput& input = psbt.inputs[input_index]; if (input.non_witness_utxo) { diff --git a/src/psbt.h b/src/psbt.h index 4777289c9114..a79a8f3a1456 100644 --- a/src/psbt.h +++ b/src/psbt.h @@ -869,7 +869,7 @@ PrecomputedTransactionData PrecomputePSBTData(const PartiallySignedTransaction& bool PSBTInputSigned(const PSBTInput& input); /** Checks whether a PSBTInput is already signed by doing script verification using final fields. */ -bool PSBTInputSignedAndVerified(const PartiallySignedTransaction psbt, unsigned int input_index, const PrecomputedTransactionData* txdata); +bool PSBTInputSignedAndVerified(const PartiallySignedTransaction& psbt, unsigned int input_index, const PrecomputedTransactionData* txdata); /** Signs a PSBTInput, verifying that all provided data matches what is being signed. * diff --git a/src/qt/bitcoin.cpp b/src/qt/bitcoin.cpp index 734a45a6c9d8..61c4e825ea43 100644 --- a/src/qt/bitcoin.cpp +++ b/src/qt/bitcoin.cpp @@ -119,7 +119,12 @@ static QString GetLangTerritory() if(!lang_territory_qsettings.isEmpty()) lang_territory = lang_territory_qsettings; // 3) -lang command line argument - lang_territory = QString::fromStdString(gArgs.GetArg("-lang", lang_territory.toStdString())); + try { + lang_territory = QString::fromStdString(gArgs.GetArg("-lang", lang_territory.toStdString())); + } catch (const std::exception&) { + // Keep the language selected above if "lang" in settings.json has an + // unexpected type. OptionsModel::Init reports it as an error. + } return lang_territory; } diff --git a/src/qt/optionsmodel.cpp b/src/qt/optionsmodel.cpp index e0f5adc3a690..12e077381cb7 100644 --- a/src/qt/optionsmodel.cpp +++ b/src/qt/optionsmodel.cpp @@ -204,8 +204,20 @@ void OptionsModel::addOverriddenOption(const std::string &option) // Writes all missing QSettings with their default values bool OptionsModel::Init(bilingual_str& error) { + // Handles exceptions thrown by univalue that can happen if settings in + // settings.json don't have the expected types. + const auto setting_error = [&](const std::string& setting, const std::exception& e) { + error.original = strprintf("Could not read setting \"%s\", %s.", setting, e.what()); + error.translated = tr("Could not read setting \"%1\", %2.").arg(QString::fromStdString(setting), e.what()).toStdString(); + return false; + }; + // Initialize display settings from stored settings. - language = QString::fromStdString(SettingToString(node().getPersistentSetting("lang"), "")); + try { + language = QString::fromStdString(SettingToString(node().getPersistentSetting("lang"), "")); + } catch (const std::exception& e) { + return setting_error("lang", e); + } checkAndMigrate(); @@ -256,9 +268,13 @@ bool OptionsModel::Init(bilingual_str& error) addOverriddenOption("-font-family"); } if (GUIUtil::fontsLoaded()) { - const QString font_name = QString::fromStdString( - SettingToString(node().getPersistentSetting("font-family"), "")); - GUIUtil::setActiveFont(font_name); + try { + const QString font_name = QString::fromStdString( + SettingToString(node().getPersistentSetting("font-family"), "")); + GUIUtil::setActiveFont(font_name); + } catch (const std::exception& e) { + return setting_error("font-family", e); + } } // Font Scale @@ -266,7 +282,11 @@ bool OptionsModel::Init(bilingual_str& error) addOverriddenOption("-font-scale"); } if (GUIUtil::fontsLoaded()) { - GUIUtil::setFontScale(SettingToInt(node().getPersistentSetting("font-scale"), GUIUtil::defaultFontScale())); + try { + GUIUtil::setFontScale(SettingToInt(node().getPersistentSetting("font-scale"), GUIUtil::defaultFontScale())); + } catch (const std::exception& e) { + return setting_error("font-scale", e); + } } // Font Weight (Normal) @@ -276,11 +296,17 @@ bool OptionsModel::Init(bilingual_str& error) const bool override_family{isOptionOverridden("-font-family")}; if (GUIUtil::fontsLoaded()) { - // If font was overridden by CLI but weight wasn't, use the font's default weight const int default_arg = GUIUtil::defaultWeightArg(GUIUtil::FontWeight::Normal); - const int arg = (!override_family || isOptionOverridden("-font-weight-normal")) - ? SettingToInt(node().getPersistentSetting("font-weight-normal"), default_arg) - : default_arg; + // Read the stored value even when it is not used below, so that a + // malformed one is reported here rather than thrown later via gArgs. + int setting_arg; + try { + setting_arg = SettingToInt(node().getPersistentSetting("font-weight-normal"), default_arg); + } catch (const std::exception& e) { + return setting_error("font-weight-normal", e); + } + // If font was overridden by CLI but weight wasn't, use the font's default weight + const int arg = (!override_family || isOptionOverridden("-font-weight-normal")) ? setting_arg : default_arg; if (!GUIUtil::setWeightFromArg(GUIUtil::FontWeight::Normal, arg)) { node().forceSetting("font-weight-normal", default_arg); GUIUtil::setWeightFromArg(GUIUtil::FontWeight::Normal, default_arg); @@ -292,11 +318,17 @@ bool OptionsModel::Init(bilingual_str& error) addOverriddenOption("-font-weight-bold"); } if (GUIUtil::fontsLoaded()) { - // If font was overridden by CLI but weight wasn't, use the font's default weight const int default_arg = GUIUtil::defaultWeightArg(GUIUtil::FontWeight::Bold); - const int arg = (!override_family || isOptionOverridden("-font-weight-bold")) - ? SettingToInt(node().getPersistentSetting("font-weight-bold"), default_arg) - : default_arg; + // Read the stored value even when it is not used below, so that a + // malformed one is reported here rather than thrown later via gArgs. + int setting_arg; + try { + setting_arg = SettingToInt(node().getPersistentSetting("font-weight-bold"), default_arg); + } catch (const std::exception& e) { + return setting_error("font-weight-bold", e); + } + // If font was overridden by CLI but weight wasn't, use the font's default weight + const int arg = (!override_family || isOptionOverridden("-font-weight-bold")) ? setting_arg : default_arg; if (!GUIUtil::setWeightFromArg(GUIUtil::FontWeight::Bold, arg)) { node().forceSetting("font-weight-bold", default_arg); GUIUtil::setWeightFromArg(GUIUtil::FontWeight::Bold, default_arg); @@ -361,11 +393,7 @@ bool OptionsModel::Init(bilingual_str& error) try { getOption(option); } catch (const std::exception& e) { - // This handles exceptions thrown by univalue that can happen if - // settings in settings.json don't have the expected types. - error.original = strprintf("Could not read setting \"%s\", %s.", setting, e.what()); - error.translated = tr("Could not read setting \"%1\", %2.").arg(QString::fromStdString(setting), e.what()).toStdString(); - return false; + return setting_error(setting, e); } } diff --git a/src/qt/test/optiontests.cpp b/src/qt/test/optiontests.cpp index fba2d2f118f6..626b5d1079d1 100644 --- a/src/qt/test/optiontests.cpp +++ b/src/qt/test/optiontests.cpp @@ -101,6 +101,61 @@ void OptionTests::integerGetArgBug() gArgs.WriteSettingsFile(); } +void OptionTests::invalidFontSetting() +{ + if (QApplication::platformName() == "minimal") { + QSKIP("AppTests cannot initialize fonts with the 'minimal' platform plugin."); + } + + // Font settings are only read once loadFonts() has run, which AppTests does. + QVERIFY2(GUIUtil::fontsLoaded(), + "GUIUtil::loadFonts() must succeed in AppTests::appTests() before OptionTests run."); + + for (const std::string setting : {"font-family", "font-scale", "font-weight-normal", "font-weight-bold"}) { + gArgs.LockSettings([&](util::Settings& settings) { + settings.rw_settings[setting] = UniValue{UniValue::VARR}; + }); + bilingual_str error; + QVERIFY(!OptionsModel{m_node}.Init(error)); + QCOMPARE(QString::fromStdString(error.original), + QString::fromStdString(strprintf("Could not read setting \"%s\", JSON value of type array is not of expected type string.", setting))); + gArgs.LockSettings([&](util::Settings& settings) { + settings.rw_settings.erase(setting); + }); + } + + // With -font-family on the command line the stored weights are not used, but + // dash-qt still reads them through gArgs later, so they must be reported too. + for (const std::string setting : {"font-weight-normal", "font-weight-bold"}) { + gArgs.LockSettings([&](util::Settings& settings) { + settings.command_line_options["font-family"] = {UniValue{GUIUtil::defaultFontFamily().toStdString()}}; + settings.rw_settings[setting] = UniValue{UniValue::VARR}; + }); + bilingual_str error; + QVERIFY(!OptionsModel{m_node}.Init(error)); + QCOMPARE(QString::fromStdString(error.original), + QString::fromStdString(strprintf("Could not read setting \"%s\", JSON value of type array is not of expected type string.", setting))); + gArgs.LockSettings([&](util::Settings& settings) { + settings.command_line_options.erase("font-family"); + settings.rw_settings.erase(setting); + }); + } +} + +void OptionTests::invalidLangSetting() +{ + // A "lang" value with an unexpected type in settings.json must be reported + // through the error string like other settings instead of throwing out of + // OptionsModel::Init. + gArgs.LockSettings([&](util::Settings& settings) { + settings.rw_settings["lang"] = UniValue{UniValue::VOBJ}; + }); + bilingual_str error; + QVERIFY(!OptionsModel{m_node}.Init(error)); + QCOMPARE(QString::fromStdString(error.original), + QString{"Could not read setting \"lang\", JSON value of type object is not of expected type string."}); +} + void OptionTests::parametersInteraction() { // Test that the bug https://github.com/bitcoin-core/gui/issues/567 does not resurface. diff --git a/src/qt/test/optiontests.h b/src/qt/test/optiontests.h index 89976d97d0b3..2cee85ab5903 100644 --- a/src/qt/test/optiontests.h +++ b/src/qt/test/optiontests.h @@ -21,6 +21,8 @@ private Q_SLOTS: void init(); // called before each test function execution. void migrateSettings(); void integerGetArgBug(); + void invalidFontSetting(); + void invalidLangSetting(); void parametersInteraction(); void extractFilter(); void effectivePointSize(); diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index 6c2a5efc689f..3b82e308a89d 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -1685,6 +1685,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 1cba059e4732..260e3875ec50 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -325,6 +325,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 ffb820e119ce..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", @@ -1959,12 +1959,18 @@ static RPCHelpMan protx_listdiff() UniValue jupdatedMNs(UniValue::VARR); for(const auto& [internal_id, stateDiff] : mnDiff.updatedMNs) { - auto dmn = baseBlockMNList.GetMNByInternalId(internal_id); + auto dmn = blockMNList.GetMNByInternalId(internal_id); // BuildDiff will construct itself with MNs that we already have knowledge // of, meaning that fetch operations should never fail. CHECK_NONFATAL(dmn); + UniValue diff_json{stateDiff.ToJson(dmn->nType)}; + if ((stateDiff.fields & CDeterministicMNStateDiff::Field_netInfo) && !dmn->pdmnState->netInfo->IsEmpty()) { + // A legacy EvoNode's Platform addresses pair its core P2P address with the Platform ports, + // which the diff doesn't carry unless they changed too + diff_json.pushKV("addresses", GetNetInfoWithLegacyFields(*dmn->pdmnState, dmn->nType)); + } UniValue obj(UniValue::VOBJ); - obj.pushKV(dmn->proTxHash.ToString(), stateDiff.ToJson(dmn->nType)); + obj.pushKV(dmn->proTxHash.ToString(), diff_json); jupdatedMNs.push_back(obj); } ret.pushKV("updatedMNs", jupdatedMNs); @@ -2150,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.", { @@ -2189,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"}, @@ -2198,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"); } @@ -2233,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)); @@ -2247,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", @@ -2311,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" @@ -2442,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/rpc/quorums.cpp b/src/rpc/quorums.cpp index 67e9da8de518..2100cbca2668 100644 --- a/src/rpc/quorums.cpp +++ b/src/rpc/quorums.cpp @@ -387,7 +387,7 @@ static RPCHelpMan quorum_dkgstatus() if (node.active_ctx) { int quorumHeight = tipHeight - (tipHeight % llmq_params.dkgInterval) + quorumIndex; if (quorumHeight <= tipHeight) { - const CBlockIndex* pQuorumBaseBlockIndex = WITH_LOCK(cs_main, return chainman.ActiveChain()[quorumHeight]); + const CBlockIndex* pQuorumBaseBlockIndex = pindexTip->GetAncestor(quorumHeight); obj.pushKV("pQuorumBaseBlockIndex", pQuorumBaseBlockIndex->nHeight); obj.pushKV("quorumHash", pQuorumBaseBlockIndex->GetBlockHash().ToString()); obj.pushKV("pindexTip", pindexTip->nHeight); @@ -427,7 +427,8 @@ static RPCHelpMan quorum_dkgstatus() } LOCK(cs_main); - std::optional> vfqc = llmq_ctx.quorum_block_processor->GetMineableCommitments(llmq_params, tipHeight + 1); + std::optional> vfqc = + llmq_ctx.quorum_block_processor->GetMineableCommitments(llmq_params, chainman.ActiveChain().Height() + 1); if (vfqc.has_value()) { for (const auto& fqc : vfqc.value()) { minableCommitments.push_back(fqc.ToJson()); diff --git a/src/test/coinjoin_inouts_tests.cpp b/src/test/coinjoin_inouts_tests.cpp index ee7b268831f6..9bf2f803869b 100644 --- a/src/test/coinjoin_inouts_tests.cpp +++ b/src/test/coinjoin_inouts_tests.cpp @@ -19,6 +19,7 @@ #include