diff --git a/src/consensus/params.h b/src/consensus/params.h index 35771762fe75..1e3fe8c4f30c 100644 --- a/src/consensus/params.h +++ b/src/consensus/params.h @@ -122,6 +122,8 @@ struct Params { uint32_t exploit_fix_2_height = 0; /** Exploit fix 3 */ uint32_t exploit_fix_3_time = 0xffffffff; + /** Time dev fund is removed and inflation reduced */ + uint32_t inflation_adjust_time = 0xffffffff; /** Last prefork anonoutput index */ int64_t m_frozen_anon_index = 0; /** Last block height of prefork blinded txns */ diff --git a/src/consensus/tx_verify.cpp b/src/consensus/tx_verify.cpp index a7ba3759af35..29fc3690a065 100644 --- a/src/consensus/tx_verify.cpp +++ b/src/consensus/tx_verify.cpp @@ -432,7 +432,8 @@ bool Consensus::CheckTxInputs(const CTransaction& tx, TxValidationState& state, if (nRingCTOutputs > 0 || nCTOutputs > 0) { return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-txns-frozen-blinded-out"); } - if (spends_tainted_blinded && nPlainValueOut + txfee > state.m_consensus_params->m_max_tainted_value_out) { + if (spends_tainted_blinded && + (state.m_exploit_fix_3 || nPlainValueOut + txfee > state.m_consensus_params->m_max_tainted_value_out)) { return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-txns-frozen-blinded-too-large"); } /* TODO? Limit to spending one frozen output at a time diff --git a/src/consensus/validation.h b/src/consensus/validation.h index a99b4302c9c0..13a49270ba0a 100644 --- a/src/consensus/validation.h +++ b/src/consensus/validation.h @@ -181,6 +181,7 @@ class ValidationState bool m_clamp_tx_version = false; bool m_exploit_fix_1 = false; bool m_exploit_fix_2 = false; + bool m_exploit_fix_3{false}; // inflation_adjust_time bool m_in_block = false; bool m_check_equal_rct_txid = true; bool m_punish_for_duplicates = false; @@ -204,6 +205,7 @@ class ValidationState m_clamp_tx_version = time >= consensusParams.clamp_tx_version_time; m_exploit_fix_1 = time >= consensusParams.exploit_fix_1_time; m_exploit_fix_2 = time >= consensusParams.exploit_fix_2_time; + m_exploit_fix_3 = time >= consensusParams.inflation_adjust_time; if (m_in_block && m_time < 1632177542) { m_check_equal_rct_txid = false; } @@ -229,6 +231,7 @@ class ValidationState m_clamp_tx_version = state_from.m_clamp_tx_version; m_exploit_fix_1 = state_from.m_exploit_fix_1; m_exploit_fix_2 = state_from.m_exploit_fix_2; + m_exploit_fix_3 = state_from.m_exploit_fix_3; m_check_equal_rct_txid = state_from.m_check_equal_rct_txid; m_punish_for_duplicates = state_from.m_punish_for_duplicates; } diff --git a/src/kernel/chainparams.cpp b/src/kernel/chainparams.cpp index 42c1ff02f72c..1b2fecd74d38 100644 --- a/src/kernel/chainparams.cpp +++ b/src/kernel/chainparams.cpp @@ -35,8 +35,16 @@ int64_t CChainParams::GetCoinYearReward(int64_t nTime) const static const int64_t nSecondsInYear = 365 * 24 * 60 * 60; if (GetChainType() != ChainType::REGTEST) { - // After HF2: 8%, 8%, 7%, 7%, 6% + if (nTime >= consensus.inflation_adjust_time) { + // After HF3: 3.5, 3.0 .. 1.5, 1.0 + int64_t nPeriodsSinceHF3 = (nTime - consensus.inflation_adjust_time) / nSecondsInYear; + if (nPeriodsSinceHF3 >= 0 && nPeriodsSinceHF3 < 6) { + return (7 - nPeriodsSinceHF3) * (CENT / 2); + } + return 1 * CENT; + } if (nTime >= consensus.exploit_fix_2_time) { + // After HF2: 8%, 8%, 7%, 7%, 6% int64_t nPeriodsSinceHF2 = (nTime - consensus.exploit_fix_2_time) / (nSecondsInYear * 2); if (nPeriodsSinceHF2 >= 0 && nPeriodsSinceHF2 < 2) { return (8 - nPeriodsSinceHF2) * CENT; @@ -526,6 +534,7 @@ class CMainParams : public CChainParams { consensus.clamp_tx_version_time = 1643734800; // 2022-02-01 17:00:00 UTC consensus.exploit_fix_3_time = 1643734800; // 2022-02-01 17:00:00 UTC consensus.m_taproot_time = 1643734800; // 2022-02-01 17:00:00 UTC + consensus.inflation_adjust_time = 1769947200; // 2026-02-01 12:00:00 UTC consensus.m_frozen_anon_index = 27340; consensus.m_frozen_blinded_height = 884433; @@ -610,7 +619,8 @@ class CMainParams : public CChainParams { particl::TreasuryFundSettings("RBiiQBnQsVPPQkUaJVQTjsZM9K2xMKozST", 10, 60)); vTreasuryFundSettings.emplace_back(consensus.exploit_fix_2_time, particl::TreasuryFundSettings("RQYUDd3EJohpjq62So4ftcV5XZfxZxJPe9", 50, 650)); - + vTreasuryFundSettings.emplace_back(consensus.inflation_adjust_time, + particl::TreasuryFundSettings("RQYUDd3EJohpjq62So4ftcV5XZfxZxJPe9", 0, 650, 1 * COIN)); base58Prefixes[PUBKEY_ADDRESS] = {0x38}; // P base58Prefixes[SCRIPT_ADDRESS] = {0x3c}; diff --git a/src/kernel/chainparams.h b/src/kernel/chainparams.h index 0b9e7abc4d1f..747395fab6c8 100644 --- a/src/kernel/chainparams.h +++ b/src/kernel/chainparams.h @@ -43,10 +43,13 @@ class TreasuryFundSettings public: TreasuryFundSettings(std::string sAddrTo, int nMinTreasuryStakePercent_, int nTreasuryOutputPeriod_) : sTreasuryFundAddresses(sAddrTo), nMinTreasuryStakePercent(nMinTreasuryStakePercent_), nTreasuryOutputPeriod(nTreasuryOutputPeriod_) {}; + TreasuryFundSettings(std::string sAddrTo, int nMinTreasuryStakePercent_, int nTreasuryOutputPeriod_, CAmount nMinPayoutAmount_) + : sTreasuryFundAddresses(sAddrTo), nMinTreasuryStakePercent(nMinTreasuryStakePercent_), nTreasuryOutputPeriod(nTreasuryOutputPeriod_), nMinPayoutAmount(nMinPayoutAmount_) {}; std::string sTreasuryFundAddresses; int nMinTreasuryStakePercent; // [0, 100] int nTreasuryOutputPeriod; // treasury fund output is created every n blocks + CAmount nMinPayoutAmount{0}; }; } // namespace particl diff --git a/src/rpc/node.cpp b/src/rpc/node.cpp index 06b083204296..2f3e6c3cb72c 100644 --- a/src/rpc/node.cpp +++ b/src/rpc/node.cpp @@ -97,6 +97,7 @@ static RPCHelpMan pushtreasuryfundsetting() {"fundaddress", RPCArg::Type::STR, RPCArg::Optional::NO, "Address accumulated treasury fund coin is paid out to"}, {"minstakepercent", RPCArg::Type::NUM, RPCArg::Optional::NO, "Minimum percentage of the block reward allocated to treasury fund"}, {"outputperiod", RPCArg::Type::NUM, RPCArg::Optional::NO, "Blocks between treasury fund outputs"}, + {"minpayoutvalue", RPCArg::Type::AMOUNT, RPCArg::Default{0}, "Minimum payment output value. Skip payment if treasury amount is less than."}, }, }, }, @@ -107,22 +108,27 @@ static RPCHelpMan pushtreasuryfundsetting() if (!Params().IsMockableChain()) { throw std::runtime_error("pushtreasuryfundsetting is for regression testing (-regtest mode) only"); } - const UniValue &setting = request.params[0].get_obj(); - RPCTypeCheckObj(setting, + const UniValue &options = request.params[0].get_obj(); + RPCTypeCheckObj(options, { {"timefrom", UniValueType(UniValue::VNUM)}, {"fundaddress", UniValueType(UniValue::VSTR)}, {"minstakepercent", UniValueType(UniValue::VNUM)}, {"outputperiod", UniValueType(UniValue::VNUM)}, - }); + }, false /* allow null */, false /* strict */); LOCK(cs_main); - particl::TreasuryFundSettings settings(setting["fundaddress"].get_str(), setting["minstakepercent"].getInt(), setting["outputperiod"].getInt()); - RegtestParams().PushTreasuryFundSettings(setting["timefrom"].getInt(), settings); + CAmount minpayoutvalue{0}; + if (options.exists("minpayoutvalue")) { + minpayoutvalue = AmountFromValue(options["minpayoutvalue"]); + } + + particl::TreasuryFundSettings settings(options["fundaddress"].get_str(), options["minstakepercent"].getInt(), options["outputperiod"].getInt(), minpayoutvalue); + RegtestParams().PushTreasuryFundSettings(options["timefrom"].getInt(), settings); - LogPrintf("Added treasury fund setting from %d: (%s, %d, %d)\n", - setting["timefrom"].getInt(), setting["fundaddress"].get_str(), setting["minstakepercent"].getInt(), setting["outputperiod"].getInt()); + LogPrintf("Added treasury fund setting from %d: (%s, %d, %d, %d)\n", + options["timefrom"].getInt(), options["fundaddress"].get_str(), options["minstakepercent"].getInt(), options["outputperiod"].getInt(), minpayoutvalue); return NullUniValue; }, diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index d49b712cda54..be48126f3565 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -342,6 +342,7 @@ static std::vector DecodeTxDoc(const std::string& txid_field_doc) {RPCResult::Type::STR_HEX, "data_hex", /*optional=*/true, "Data component"}, {RPCResult::Type::STR_AMOUNT, "ct_fee", /*optional=*/true, "data - SMSG confidential transaction fee"}, {RPCResult::Type::STR_AMOUNT, "smsgfeerate", /*optional=*/true, "data - SMSG fee rate"}, + {RPCResult::Type::STR_AMOUNT, "treasury_fund_cfwd", /*optional=*/true, "data - Treasury fund carried forward"}, {RPCResult::Type::STR_HEX, "smsgdifficulty", /*optional=*/true, "data - SMSG difficulty"}, {RPCResult::Type::STR, "vote", /*optional=*/true, "data - Voting entry"}, {RPCResult::Type::STR_HEX, "pubkey", /*optional=*/true, "pubkey for anon output"}, diff --git a/src/test/particlchain_tests.cpp b/src/test/particlchain_tests.cpp index f50053683600..e7d84e90e332 100644 --- a/src/test/particlchain_tests.cpp +++ b/src/test/particlchain_tests.cpp @@ -469,7 +469,24 @@ BOOST_AUTO_TEST_CASE(coin_year_reward) BOOST_CHECK(Params().GetCoinYearReward(1626109200 + seconds_in_year * 3) == 7 * CENT); BOOST_CHECK(Params().GetCoinYearReward(1626109200 + seconds_in_year * 4 - 1) == 7 * CENT); BOOST_CHECK(Params().GetCoinYearReward(1626109200 + seconds_in_year * 4) == 6 * CENT); // 2025-07-11 17:00:00 UTC - BOOST_CHECK(Params().GetCoinYearReward(1626109200 + seconds_in_year * 6) == 6 * CENT); + + int64_t hf3_time = Params().GetConsensus().inflation_adjust_time; + BOOST_CHECK(Params().GetCoinYearReward(hf3_time - 1) == 6 * CENT); + auto check_rate_at_time = [&](int64_t time, CAmount expect_value, const char *expect_time_str = nullptr) { + BOOST_CHECK_EQUAL(Params().GetCoinYearReward(time), expect_value); + if (expect_time_str) { + BOOST_CHECK_EQUAL(FormatISO8601DateTime(time), expect_time_str); + } + }; + check_rate_at_time(hf3_time - 1, 6 * CENT); + check_rate_at_time(hf3_time, 3.5 * CENT, "2026-02-01T12:00:00Z"); + check_rate_at_time(hf3_time + seconds_in_year * 1 - 1, 3.5 * CENT); + check_rate_at_time(hf3_time + seconds_in_year * 1, 3 * CENT, "2027-02-01T12:00:00Z"); + check_rate_at_time(hf3_time + seconds_in_year * 2, 2.5 * CENT, "2028-02-01T12:00:00Z"); + check_rate_at_time(hf3_time + seconds_in_year * 3, 2 * CENT, "2029-01-31T12:00:00Z"); + check_rate_at_time(hf3_time + seconds_in_year * 4, 1.5 * CENT, "2030-01-31T12:00:00Z"); + check_rate_at_time(hf3_time + seconds_in_year * 5, 1 * CENT, "2031-01-31T12:00:00Z"); + check_rate_at_time(hf3_time + seconds_in_year * 6, 1 * CENT, "2032-01-31T12:00:00Z"); } BOOST_AUTO_TEST_CASE(taproot) diff --git a/src/validation.cpp b/src/validation.cpp index 23e444762dde..066265081e4c 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -3327,7 +3327,7 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state, } } - if (!pTreasuryFundSettings || pTreasuryFundSettings->nMinTreasuryStakePercent <= 0) { + if (!pTreasuryFundSettings || pTreasuryFundSettings->nMinTreasuryStakePercent < 0) { if (nStakeReward < 0 || nStakeReward > nCalculatedStakeReward) { LogPrintf("ERROR: %s: Coinstake pays too much(actual=%d vs calculated=%d)\n", __func__, nStakeReward, nCalculatedStakeReward); return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-cs-amount"); @@ -3356,7 +3356,8 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state, } } - if (pindex->nHeight % pTreasuryFundSettings->nTreasuryOutputPeriod == 0) { + if (pindex->nHeight % pTreasuryFundSettings->nTreasuryOutputPeriod == 0 && + nTreasuryBfwd >= pTreasuryFundSettings->nMinPayoutAmount) { // Fund output must exist and match cfwd, cfwd data output must be unset // nStakeReward must == nTreasuryBfwd + nCalculatedStakeReward @@ -3394,20 +3395,19 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state, // Ensure cfwd data output is correct and nStakeReward is <= nHolderPart // cfwd must == nTreasuryBfwd + (nCalculatedStakeReward - nStakeReward) // Allowing users to set a higher split + CAmount nTreasuryCfwd = nTreasuryBfwd + nCalculatedStakeReward - nStakeReward; if (nStakeReward < 0 || nStakeReward > nMaxHolderPart) { LogPrintf("ERROR: %s: Bad stake-reward (actual=%d vs maxholderpart=%d)\n", __func__, nStakeReward, nMaxHolderPart); return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-cs-amount"); } - CAmount nTreasuryCfwd = nTreasuryBfwd + nCalculatedStakeReward - nStakeReward; if (!txCoinstake->GetTreasuryFundCfwd(nTreasuryCfwdCheck) || nTreasuryCfwdCheck != nTreasuryCfwd) { LogPrintf("ERROR: %s: Coinstake treasury fund carried forward mismatch (actual=%d vs expected=%d)\n", __func__, nTreasuryCfwdCheck, nTreasuryCfwd); return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-cs-cfwd"); } } - - particl::coinStakeCache.InsertCoinStake(blockHash, txCoinstake); } + particl::coinStakeCache.InsertCoinStake(blockHash, txCoinstake); } else { if (blockHash != params.GetConsensus().hashGenesisBlock) { LogPrintf("ERROR: %s: Block isn't coinstake or genesis.\n", __func__); diff --git a/src/wallet/hdwallet.cpp b/src/wallet/hdwallet.cpp index 3b66bafb1eb9..e4628f22cb98 100644 --- a/src/wallet/hdwallet.cpp +++ b/src/wallet/hdwallet.cpp @@ -11910,6 +11910,8 @@ void CHDWallet::AvailableBlindedCoins(std::vector& vCoins, const CCoin vCoins.clear(); + int64_t time_now = GetTime(); + const int min_depth = {coinControl ? coinControl->m_min_depth : DEFAULT_MIN_DEPTH}; const int max_depth = {coinControl ? coinControl->m_max_depth : DEFAULT_MAX_DEPTH}; //const bool fIncludeImmature = {coinControl ? coinControl->m_include_immature : false}; // Blinded coins can't stake @@ -12007,7 +12009,7 @@ void CHDWallet::AvailableBlindedCoins(std::vector& vCoins, const CCoin } if (spend_frozen && !include_tainted_frozen) { - if (r.nValue > consensusParams.m_max_tainted_value_out) { + if (time_now >= consensusParams.inflation_adjust_time || r.nValue > consensusParams.m_max_tainted_value_out) { if (IsFrozenBlindOutput(txid)) { continue; } @@ -12263,7 +12265,8 @@ void CHDWallet::AvailableAnonCoins(std::vector &vCoins, const CCoinCon !stx.tx->vpout[r.n]->IsType(OUTPUT_RINGCT) || !chain().readRCTOutputLink(((CTxOutRingCT*)stx.tx->vpout[r.n].get())->pk, index) || IsBlacklistedAnonOutput(index) || - (!IsWhitelistedAnonOutput(index, time_now, consensusParams) && r.nValue > consensusParams.m_max_tainted_value_out)) { + (!IsWhitelistedAnonOutput(index, time_now, consensusParams) && + (time_now >= consensusParams.inflation_adjust_time || r.nValue > consensusParams.m_max_tainted_value_out))) { continue; } } @@ -13738,7 +13741,7 @@ bool CHDWallet::CreateCoinStake(unsigned int nBits, int64_t nTime, int nBlockHei CTransactionRef txPrevCoinstake = nullptr; CAmount nRewardOut; const particl::TreasuryFundSettings *pTreasuryFundSettings = Params().GetTreasuryFundSettings(nTime); - if (!pTreasuryFundSettings || pTreasuryFundSettings->nMinTreasuryStakePercent <= 0) { + if (!pTreasuryFundSettings || pTreasuryFundSettings->nMinTreasuryStakePercent < 0) { nRewardOut = nReward; } else { int64_t nStakeSplit = std::max(pTreasuryFundSettings->nMinTreasuryStakePercent, nWalletTreasuryFundCedePercent); @@ -13759,7 +13762,8 @@ bool CHDWallet::CreateCoinStake(unsigned int nBits, int64_t nTime, int nBlockHei } CAmount nTreasuryCfwd = nTreasuryBfwd + nTreasuryPart; - if (nBlockHeight % pTreasuryFundSettings->nTreasuryOutputPeriod == 0) { + if (nBlockHeight % pTreasuryFundSettings->nTreasuryOutputPeriod == 0 && + nTreasuryBfwd >= pTreasuryFundSettings->nMinPayoutAmount) { // Place treasury fund output OUTPUT_PTR outTreasurySplit = MAKE_OUTPUT(); outTreasurySplit->nValue = nTreasuryCfwd; diff --git a/src/wallet/rpchdwallet.cpp b/src/wallet/rpchdwallet.cpp index 98b42ae61cf8..62b9f4c3ac31 100644 --- a/src/wallet/rpchdwallet.cpp +++ b/src/wallet/rpchdwallet.cpp @@ -4490,6 +4490,7 @@ static RPCHelpMan getstakinginfo() {RPCResult::Type::STR_AMOUNT, "reserve", /*optional=*/true, "The reserve balance of the wallet in " + CURRENCY_UNIT}, {RPCResult::Type::STR_AMOUNT, "wallettreasurydonationpercent", /*optional=*/true, "User set percentage of the block reward ceded to the treasury"}, {RPCResult::Type::STR_AMOUNT, "treasurydonationpercent", /*optional=*/true, "Network enforced percentage of the block reward ceded to the treasury"}, + {RPCResult::Type::STR_AMOUNT, "treasurydonationminpayout", /*optional=*/true, "Minimum (brought forward) treasury amount to trigger payout."}, {RPCResult::Type::STR_AMOUNT, "minstakeablevalue", "The minimum value for an output to attempt staking in " + CURRENCY_UNIT}, {RPCResult::Type::NUM, "minstakeabledepth", "Minimum depth required in the chain for an output to stake"}, {RPCResult::Type::NUM, "currentblocksize", "The last approximate block size in bytes"}, @@ -4583,6 +4584,7 @@ static RPCHelpMan getstakinginfo() const particl::TreasuryFundSettings *pTreasuryFundSettings = Params().GetTreasuryFundSettings(nTipTime); if (pTreasuryFundSettings && pTreasuryFundSettings->nMinTreasuryStakePercent > 0) { obj.pushKV("treasurydonationpercent", pTreasuryFundSettings->nMinTreasuryStakePercent); + obj.pushKV("treasurydonationminpayout", ValueFromAmount(pTreasuryFundSettings->nMinPayoutAmount)); } obj.pushKV("minstakeablevalue", ValueFromAmount(pwallet->m_min_stakeable_value)); diff --git a/test/functional/feature_part_treasury_fund.py b/test/functional/feature_part_treasury_fund.py index ca512ed0424f..2f18381f5b47 100755 --- a/test/functional/feature_part_treasury_fund.py +++ b/test/functional/feature_part_treasury_fund.py @@ -1,8 +1,11 @@ #!/usr/bin/env python3 -# Copyright (c) 2020-2021 tecnovert +# Copyright (c) 2020-2025 tecnovert # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. +import time +from decimal import Decimal + from test_framework.test_particl import ParticlTestFramework from test_framework.messages import COIN @@ -120,6 +123,50 @@ def get_coinstake_reward(moneysupply): assert (block_reward_14['stakereward'] * COIN == expect_reward) assert (block_reward_14['blockreward'] * COIN == expect_reward - ((expect_reward * 10) // 100)) + self.log.info('Test zero minstakepercent') + for n in nodes: + n.pushtreasuryfundsetting({'timefrom': int(time.time()), 'fundaddress': fund_addr, 'minstakepercent': 0, 'outputperiod': 2}) + staking_opts['treasurydonationpercent'] = 0 + nodes[0].walletsettings('stakingoptions', staking_opts) + nodes[0].walletsettings('stakelimit', {'height': 16}) + + self.wait_for_height(nodes[0], 16) + si = nodes[0].getstakinginfo() + block_reward_16 = nodes[0].getblockreward(16) + assert (block_reward_16['stakereward'] * COIN == expect_reward) + assert (block_reward_16['blockreward'] * COIN == expect_reward) + + # Payout should still occur (in block 16) + coinstake_15 = nodes[0].decoderawtransaction(nodes[0].getrawtransaction(nodes[0].getblockreward(15)["coinstake"])) + assert "treasury_fund_cfwd" in coinstake_15["vout"][0] + for n in range(1, len(coinstake_15["vout"])): + assert coinstake_15["vout"][n]["scriptPubKey"]["address"] != fund_addr + + coinstake_16 = nodes[0].decoderawtransaction(nodes[0].getrawtransaction(block_reward_16["coinstake"])) + assert "treasury_fund_cfwd" not in coinstake_16["vout"][0] + assert coinstake_16["vout"][1]["scriptPubKey"]["address"] == fund_addr + + for n in nodes: + n.pushtreasuryfundsetting({'timefrom': int(time.time()), 'fundaddress': fund_addr, 'minstakepercent': 0, 'outputperiod': 2}) + + self.log.info('Test mintreasurypayout') # payout should skip block 18 + for n in nodes: + min_payout_value = Decimal(expect_reward * 3) / COIN + n.pushtreasuryfundsetting({'timefrom': int(time.time()), 'fundaddress': fund_addr, 'minstakepercent': 0, 'outputperiod': 2, 'minpayoutvalue': min_payout_value}) + + staking_opts['treasurydonationpercent'] = 100 + nodes[0].walletsettings('stakingoptions', staking_opts) + nodes[0].walletsettings('stakelimit', {'height': 20}) + self.wait_for_height(nodes[0], 20) + coinstake_18 = nodes[0].decoderawtransaction(nodes[0].getrawtransaction(nodes[0].getblockreward(18)["coinstake"])) + assert "treasury_fund_cfwd" in coinstake_18["vout"][0] + for n in range(1, len(coinstake_18["vout"])): + assert coinstake_18["vout"][n]["scriptPubKey"]["address"] != fund_addr + + coinstake_20 = nodes[0].decoderawtransaction(nodes[0].getrawtransaction(nodes[0].getblockreward(20)["coinstake"])) + assert "treasury_fund_cfwd" not in coinstake_20["vout"][0] + assert coinstake_20["vout"][1]["scriptPubKey"]["address"] == fund_addr + if __name__ == '__main__': TreasuryFundTest().main()