diff --git a/src/avalanche/peermanager.cpp b/src/avalanche/peermanager.cpp --- a/src/avalanche/peermanager.cpp +++ b/src/avalanche/peermanager.cpp @@ -165,7 +165,8 @@ static bool isOrphanState(const ProofValidationState &state) { return state.GetResult() == ProofValidationResult::MISSING_UTXO || - state.GetResult() == ProofValidationResult::HEIGHT_MISMATCH; + state.GetResult() == ProofValidationResult::HEIGHT_MISMATCH || + state.GetResult() == ProofValidationResult::IMMATURE_UTXO; } bool PeerManager::updateNextPossibleConflictTime( diff --git a/src/avalanche/proof.h b/src/avalanche/proof.h --- a/src/avalanche/proof.h +++ b/src/avalanche/proof.h @@ -32,6 +32,13 @@ */ static constexpr bool AVALANCHE_DEFAULT_LEGACY_PROOF = true; +/** + * Minimum number of confirmations before a stake utxo is mature enough to be + * included into a proof. + * FIXME: Set to sane default after the tests have been updated. + */ +static constexpr int AVALANCHE_DEFAULT_STAKE_UTXO_CONFIRMATIONS = 0; + namespace avalanche { /** Minimum amount per utxo */ diff --git a/src/avalanche/proof.cpp b/src/avalanche/proof.cpp --- a/src/avalanche/proof.cpp +++ b/src/avalanche/proof.cpp @@ -13,6 +13,7 @@ #include <util/strencodings.h> #include <util/system.h> #include <util/translation.h> +#include <validation.h> // For g_chainman #include <tinyformat.h> @@ -185,10 +186,24 @@ return false; } + const int64_t activeHeight = + WITH_LOCK(cs_main, return g_chainman.ActiveHeight()); + const int64_t stakeUtxoMinConfirmations = + gArgs.GetArg("-avaproofstakeutxoconfirmations", + AVALANCHE_DEFAULT_STAKE_UTXO_CONFIRMATIONS); + for (const SignedStake &ss : stakes) { const Stake &s = ss.getStake(); const COutPoint &utxo = s.getUTXO(); + if ((s.getHeight() + stakeUtxoMinConfirmations) > activeHeight) { + return state.Invalid( + ProofValidationResult::IMMATURE_UTXO, "immature-utxo", + strprintf("TxId: %s, block height: %d, chaintip height: %d", + s.getUTXO().GetTxId().ToString(), s.getHeight(), + activeHeight)); + } + Coin coin; if (!view.GetCoin(utxo, coin)) { // The coins are not in the UTXO set. diff --git a/src/avalanche/test/init_tests.cpp b/src/avalanche/test/init_tests.cpp --- a/src/avalanche/test/init_tests.cpp +++ b/src/avalanche/test/init_tests.cpp @@ -39,6 +39,8 @@ BOOST_CHECK_EQUAL(args.GetBoolArg("-enableavalanche", false), true); BOOST_CHECK_EQUAL(args.GetBoolArg("-legacyavaproof", true), false); + BOOST_CHECK_EQUAL(args.GetArg("-avaproofstakeutxoconfirmations", 42), + 2016); BOOST_CHECK_EQUAL( args.GetBoolArg("-enableavalanchepeerdiscovery", false), true); BOOST_CHECK_EQUAL( @@ -60,6 +62,8 @@ BOOST_CHECK_EQUAL(args.GetBoolArg("-enableavalanche", true), false); BOOST_CHECK_EQUAL(args.GetBoolArg("-legacyavaproof", false), AVALANCHE_DEFAULT_LEGACY_PROOF); + BOOST_CHECK_EQUAL(args.GetArg("-avaproofstakeutxoconfirmations", 42), + AVALANCHE_DEFAULT_STAKE_UTXO_CONFIRMATIONS); BOOST_CHECK_EQUAL( args.GetBoolArg("-enableavalanchepeerdiscovery", true), false); BOOST_CHECK_EQUAL( @@ -79,6 +83,7 @@ ArgsManager args; args.ForceSetArg("-avalanche", "1"); args.ForceSetArg("-legacyavaproof", "1"); + args.ForceSetArg("-avaproofstakeutxoconfirmations", "500"); args.ForceSetArg("-enableavalancheproofreplacement", "0"); args.ForceSetArg("-automaticunparking", "1"); args.ForceSetArg("-avaminquorumstake", FormatMoney(123 * COIN)); @@ -87,6 +92,8 @@ BOOST_CHECK_EQUAL(args.GetBoolArg("-enableavalanche", false), true); BOOST_CHECK_EQUAL(args.GetBoolArg("-legacyavaproof", false), true); + BOOST_CHECK_EQUAL(args.GetArg("-avaproofstakeutxoconfirmations", 42), + 500); BOOST_CHECK_EQUAL( args.GetBoolArg("-enableavalanchepeerdiscovery", false), true); BOOST_CHECK_EQUAL( diff --git a/src/avalanche/test/peermanager_tests.cpp b/src/avalanche/test/peermanager_tests.cpp --- a/src/avalanche/test/peermanager_tests.cpp +++ b/src/avalanche/test/peermanager_tests.cpp @@ -117,7 +117,7 @@ } // namespace avalanche namespace { -struct NoCoolDownFixture : public TestingSetup { +struct NoCoolDownFixture : public TestChain100Setup { NoCoolDownFixture() { gArgs.ForceSetArg("-avalancheconflictingproofcooldown", "0"); } @@ -127,7 +127,7 @@ }; } // namespace -BOOST_FIXTURE_TEST_SUITE(peermanager_tests, TestingSetup) +BOOST_FIXTURE_TEST_SUITE(peermanager_tests, TestChain100Setup) BOOST_AUTO_TEST_CASE(select_peer_linear) { // No peers. @@ -694,6 +694,7 @@ } BOOST_AUTO_TEST_CASE(orphan_proofs) { + gArgs.ForceSetArg("-avaproofstakeutxoconfirmations", "1"); avalanche::PeerManager pm; auto key = CKey::MakeCompressedKey(); @@ -701,10 +702,12 @@ COutPoint outpoint1 = COutPoint(TxId(GetRandHash()), 0); COutPoint outpoint2 = COutPoint(TxId(GetRandHash()), 0); COutPoint outpoint3 = COutPoint(TxId(GetRandHash()), 0); + COutPoint outpoint4 = COutPoint(TxId(GetRandHash()), 0); const Amount v = 5 * COIN; - const int height = 1234; - const int wrongHeight = 12345; + const int height = 98; + const int wrongHeight = 99; + const int immatureHeight = 100; const auto makeProof = [&](const COutPoint &outpoint, const int h) { return buildProofWithOutpoints(key, {outpoint}, v, key, 0, h); @@ -713,10 +716,12 @@ auto proof1 = makeProof(outpoint1, height); auto proof2 = makeProof(outpoint2, height); auto proof3 = makeProof(outpoint3, wrongHeight); + auto proof4 = makeProof(outpoint4, immatureHeight); - // Add outpoints 1 and 3, not 2 + // Add outpoints, except for proof 2 addCoin(outpoint1, key, v, height); addCoin(outpoint3, key, v, height); + addCoin(outpoint4, key, v, immatureHeight); // Add the proofs BOOST_CHECK(pm.registerProof(proof1)); @@ -729,6 +734,7 @@ registerOrphan(proof2); registerOrphan(proof3); + registerOrphan(proof4); auto checkOrphan = [&](const ProofRef &proof, bool expectedOrphan) { const ProofId &proofid = proof->getId(); @@ -752,6 +758,8 @@ checkOrphan(proof2, true); // HEIGHT_MISMATCH checkOrphan(proof3, true); + // IMMATURE_UTXO + checkOrphan(proof4, true); // Add outpoint2, proof2 is no longer considered orphan addCoin(outpoint2, key, v, height); @@ -763,6 +771,11 @@ checkOrphan(proof1, false); checkOrphan(proof3, true); + // Mine a block to increase the chain height for proof4 verification + mineBlocks(1); + pm.updatedBlockTip(); + checkOrphan(proof4, false); + // Spend outpoint1, proof1 becomes orphan { LOCK(cs_main); @@ -795,8 +808,6 @@ // Track expected orphans so we can test them later std::vector<ProofRef> orphans; orphans.push_back(proof1); - orphans.push_back(proof2); - orphans.push_back(proof3); // Fill up orphan pool to test the size limit for (uint32_t i = 1; i < AVALANCHE_MAX_ORPHAN_PROOFS; i++) { @@ -834,6 +845,11 @@ CCoinsViewCache &coins = ::ChainstateActive().CoinsTip(); coins.SpendCoin(outpoint2); coins.SpendCoin(outpoint3); + coins.SpendCoin(outpoint4); + + orphans.push_back(proof2); + orphans.push_back(proof3); + orphans.push_back(proof4); } pm.updatedBlockTip(); @@ -865,6 +881,8 @@ } } BOOST_CHECK_EQUAL(numOrphans, AVALANCHE_MAX_ORPHAN_PROOFS); + + gArgs.ClearForcedArg("-avaproofstakeutxoconfirmations"); } BOOST_AUTO_TEST_CASE(dangling_node) { diff --git a/src/avalanche/test/proof_tests.cpp b/src/avalanche/test/proof_tests.cpp --- a/src/avalanche/test/proof_tests.cpp +++ b/src/avalanche/test/proof_tests.cpp @@ -11,6 +11,7 @@ #include <script/standard.h> #include <util/strencodings.h> #include <util/translation.h> +#include <validation.h> #include <test/util/setup_common.h> @@ -18,7 +19,7 @@ using namespace avalanche; -BOOST_FIXTURE_TEST_SUITE(proof_tests, TestingSetup) +BOOST_FIXTURE_TEST_SUITE(proof_tests, TestChain100Setup) BOOST_AUTO_TEST_CASE(proof_random) { for (int i = 0; i < 1000; i++) { @@ -1024,6 +1025,38 @@ BOOST_CHECK(state.GetResult() == ProofValidationResult::WRONG_STAKE_ORDERING); } + + // Immature stake + { + uint32_t chaintipHeight = ::ChainActive().Height(); + + // tuple<stake utxo confs, avaproofstakeutxoconfirmations, result> + std::vector<std::tuple<uint32_t, std::string, ProofValidationResult>> + testCases = { + // Require less or equal the number of confirmations the stake + // has + {chaintipHeight, "0", ProofValidationResult::NONE}, + {50, "0", ProofValidationResult::NONE}, + {50, "49", ProofValidationResult::NONE}, + {50, "50", ProofValidationResult::NONE}, + // Require more than the number of confirmations the stake has + {chaintipHeight, "1", ProofValidationResult::IMMATURE_UTXO}, + {50, "51", ProofValidationResult::IMMATURE_UTXO}, + {50, "100", ProofValidationResult::IMMATURE_UTXO}, + }; + + for (auto it = testCases.begin(); it != testCases.end(); ++it) { + const uint32_t stakeConfs = std::get<0>(*it); + COutPoint outpoint(TxId(InsecureRand256()), InsecureRand32()); + CTxOut output(value, GetScriptForRawPubKey(pubkey)); + coins.AddCoin(outpoint, Coin(output, stakeConfs, false), false); + + gArgs.ForceSetArg("-avaproofstakeutxoconfirmations", + std::get<1>(*it)); + runCheck(std::get<2>(*it), outpoint, value, stakeConfs, false, key); + } + gArgs.ClearForcedArg("-avaproofstakeutxoconfirmations"); + } } BOOST_AUTO_TEST_CASE(deterministic_proofid) { diff --git a/src/avalanche/test/proofpool_tests.cpp b/src/avalanche/test/proofpool_tests.cpp --- a/src/avalanche/test/proofpool_tests.cpp +++ b/src/avalanche/test/proofpool_tests.cpp @@ -18,7 +18,7 @@ using namespace avalanche; -BOOST_FIXTURE_TEST_SUITE(proofpool_tests, TestingSetup) +BOOST_FIXTURE_TEST_SUITE(proofpool_tests, TestChain100Setup) BOOST_AUTO_TEST_CASE(add_remove_proof_no_conflict) { ProofPool testPool; diff --git a/src/avalanche/test/util.cpp b/src/avalanche/test/util.cpp --- a/src/avalanche/test/util.cpp +++ b/src/avalanche/test/util.cpp @@ -22,7 +22,7 @@ const COutPoint o(TxId(GetRandHash()), 0); const Amount v = (int64_t(score) * COIN) / 100; - const int height = 1234; + const int height = 100; const bool is_coinbase = false; { diff --git a/src/avalanche/validation.h b/src/avalanche/validation.h --- a/src/avalanche/validation.h +++ b/src/avalanche/validation.h @@ -28,6 +28,7 @@ NON_STANDARD_DESTINATION, DESTINATION_NOT_SUPPORTED, DESTINATION_MISMATCH, + IMMATURE_UTXO, }; class ProofValidationState : public ValidationState<ProofValidationResult> {}; diff --git a/src/init.cpp b/src/init.cpp --- a/src/init.cpp +++ b/src/init.cpp @@ -1392,6 +1392,13 @@ strprintf("Use the legacy avalanche proof format (default: %u)", AVALANCHE_DEFAULT_LEGACY_PROOF), ArgsManager::ALLOW_BOOL, OptionsCategory::AVALANCHE); + argsman.AddArg( + "-avaproofstakeutxoconfirmations", + strprintf( + "Minimum number of confirmations before a stake utxo is mature" + " enough to be included into a proof (default: %s)", + AVALANCHE_DEFAULT_STAKE_UTXO_CONFIRMATIONS), + ArgsManager::ALLOW_INT, OptionsCategory::AVALANCHE); argsman.AddArg("-avamasterkey", "Master key associated with the proof. If a proof is " "required, this is mandatory.", @@ -1670,6 +1677,10 @@ args.SoftSetBoolArg("-legacyavaproof", fAvalanche ? false : AVALANCHE_DEFAULT_LEGACY_PROOF); + args.SoftSetArg( + "-avaproofstakeutxoconfirmations", + fAvalanche ? "2016" + : ToString(AVALANCHE_DEFAULT_STAKE_UTXO_CONFIRMATIONS)); args.SoftSetBoolArg("-enableavalanchepeerdiscovery", fAvalanche); args.SoftSetBoolArg("-enableavalancheproofreplacement", fAvalanche); args.SoftSetBoolArg("-automaticunparking", !fAvalanche); @@ -2136,6 +2147,15 @@ "No proxy server specified. Use -proxy=<ip> or -proxy=<ip:port>.")); } + // Avalanche parameters + const int64_t stakeUtxoMinConfirmations = + gArgs.GetArg("-avaproofstakeutxoconfirmations", + AVALANCHE_DEFAULT_STAKE_UTXO_CONFIRMATIONS); + if (!chainparams.IsTestChain() && stakeUtxoMinConfirmations != 2016) { + return InitError(_("Avalanche stake UTXO minimum confirmations can " + "only be set on test chains.")); + } + return true; }