From 78d6c6a06c41c17861b3ee4ac3cf4097f00ff1c4 Mon Sep 17 00:00:00 2001 From: Peter Shugalev Date: Sat, 20 Jan 2024 10:30:20 +0100 Subject: [PATCH 1/6] Introduce sporks "spark" and "sparktransparentlimit" --- src/evo/spork.cpp | 32 ++++++++++++++++++++++++++++++-- src/evo/spork.h | 2 ++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/evo/spork.cpp b/src/evo/spork.cpp index 16ebde591f..9a0ccfb400 100644 --- a/src/evo/spork.cpp +++ b/src/evo/spork.cpp @@ -65,6 +65,18 @@ static bool IsTransactionAllowed(const CTransaction &tx, const ActiveSporkMap &s } } } + else if (tx.IsSparkTransaction()) { + if (sporkMap.count(CSporkAction::featureSpark) > 0) + return state.DoS(100, false, REJECT_CONFLICT, "txn-spark-disabled", false, "Spark transactions are disabled at the moment"); + + if (tx.IsSparkSpend()) { + const auto &limitSpork = sporkMap.find(CSporkAction::featureSparkTransparentLimit); + if (limitSpork != sporkMap.cend()) { + if (spark::GetSpendTransparentAmount(tx) > (CAmount)limitSpork->second.second) + return state.DoS(100, false, REJECT_CONFLICT, "txn-spark-disabled", false, "Spark transaction is over the transparent limit"); + } + } + } return true; } @@ -176,8 +188,24 @@ bool CSporkManager::IsBlockAllowed(const CBlock &block, const CBlockIndex *pinde totalTransparentOutput += lelantus::GetSpendTransparentAmount(*tx); } - return totalTransparentOutput <= CAmount(limit) ? true : - state.DoS(100, false, REJECT_CONFLICT, "txn-lelantus-disabled", false, "Block is over the transparent output limit because of existing spork"); + if (totalTransparentOutput > CAmount(limit)) + return state.DoS(100, false, REJECT_CONFLICT, "txn-lelantus-disabled", false, "Block is over the transparent output limit because of existing spork"); + } + + if (pindex->activeDisablingSporks.count(CSporkAction::featureSparkTransparentLimit) > 0) { + // limit total transparent output of lelantus joinsplit + int64_t limit = pindex->activeDisablingSporks.at(CSporkAction::featureSparkTransparentLimit).second; + CAmount totalTransparentOutput = 0; + + for (const auto &tx: block.vtx) { + if (!tx->IsSparkSpend()) + continue; + + totalTransparentOutput += spark::GetSpendTransparentAmount(*tx); + } + + if (totalTransparentOutput > CAmount(limit)) + return state.DoS(100, false, REJECT_CONFLICT, "txn-spark-disabled", false, "Block is over the transparent output limit because of existing spork"); } return true; diff --git a/src/evo/spork.h b/src/evo/spork.h index 4087513813..4b4d67fc67 100644 --- a/src/evo/spork.h +++ b/src/evo/spork.h @@ -17,6 +17,8 @@ struct CSporkAction static constexpr const char *featureLelantusTransparentLimit = "lelantustransparentlimit"; static constexpr const char *featureChainlocks = "chainlocks"; static constexpr const char *featureInstantSend = "instantsend"; + static constexpr const char *featureSpark = "spark"; + static constexpr const char *featureSparkTransparentLimit = "sparktransparentlimit"; enum ActionType { sporkDisable = 1, From e3f30d3a56b16e4af176de420a9e88644c568420 Mon Sep 17 00:00:00 2001 From: Peter Shugalev Date: Sat, 20 Jan 2024 11:20:18 +0100 Subject: [PATCH 2/6] Added spark and sparktransparentlimit to allowed spork features --- src/rpc/rpcevo.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/rpc/rpcevo.cpp b/src/rpc/rpcevo.cpp index 14cd7d9dcb..6037b4835f 100644 --- a/src/rpc/rpcevo.cpp +++ b/src/rpc/rpcevo.cpp @@ -1405,7 +1405,9 @@ UniValue spork(const JSONRPCRequest& request) CSporkAction::featureLelantus, CSporkAction::featureChainlocks, CSporkAction::featureInstantSend, - CSporkAction::featureLelantusTransparentLimit + CSporkAction::featureLelantusTransparentLimit, + CSporkAction::featureSpark, + CSporkAction::featureSparkTransparentLimit }; for (const CSporkAction &action: sporkTx.actions) { From 9149d0ccdf9e6d60c83a931f0d605b385a4e5cda Mon Sep 17 00:00:00 2001 From: Peter Shugalev Date: Sat, 20 Jan 2024 20:06:31 +0100 Subject: [PATCH 3/6] Added test for spark spork --- src/test/evospork_tests.cpp | 252 ++++++++++++++++++++++++++++++++++++ src/test/test_bitcoin.cpp | 8 +- 2 files changed, 257 insertions(+), 3 deletions(-) diff --git a/src/test/evospork_tests.cpp b/src/test/evospork_tests.cpp index 775a1ae581..9fe24e15b6 100644 --- a/src/test/evospork_tests.cpp +++ b/src/test/evospork_tests.cpp @@ -432,4 +432,256 @@ BOOST_AUTO_TEST_CASE(startstopblock) } +BOOST_AUTO_TEST_SUITE_END() + +// Extend spork stop block to 2000 +struct SparkSporkTestingSetup : public SparkTestingSetup +{ + Consensus::Params &mutableParams; + Consensus::Params originalParams; + + SparkSporkTestingSetup() : SparkTestingSetup(), mutableParams(const_cast(Params().GetConsensus())) + { + originalParams = mutableParams; + mutableParams.nEvoSporkStopBlock = 2000; + } + + ~SparkSporkTestingSetup() { + mutableParams = originalParams; + } + +}; + +BOOST_FIXTURE_TEST_SUITE(evospork_spark_tests, SparkSporkTestingSetup) + +BOOST_AUTO_TEST_CASE(general) +{ + int prevHeight; + pwalletMain->SetBroadcastTransactions(true); + + for (int n=chainActive.Height(); n<1000; n++) + GenerateBlock({}); + + auto utxos = BuildSimpleUtxoMap(coinbaseTxns); + CMutableTransaction sporkTx1 = CreateSporkTx(utxos, coinbaseKey, { + {CSporkAction::sporkDisable, CSporkAction::featureSpark, 0, 1075} + }); + CMutableTransaction sporkTx2 = CreateSporkTx(utxos, coinbaseKey, { + {CSporkAction::sporkDisable, CSporkAction::featureSpark, 0, 1085} + }); + CMutableTransaction sporkTx3 = CreateSporkTx(utxos, coinbaseKey, { + {CSporkAction::sporkEnable, CSporkAction::featureSpark, 0, 0} + }); + + prevHeight = chainActive.Height(); + GenerateBlock({sporkTx1}); + // spork should be accepted + BOOST_ASSERT(chainActive.Height() == prevHeight+1); + + std::vector sparkMints; + GenerateMints({1*COIN, 2*COIN}, sparkMints); + + prevHeight = chainActive.Height(); + GenerateBlock(sparkMints); + // can't accept spark tx after spark + BOOST_ASSERT(chainActive.Height() == prevHeight); + + // wait until the spork expires + for (int n=chainActive.Height(); n<1075; n++) + GenerateBlock({}); + prevHeight = chainActive.Height(); + GenerateBlock({sparkMints[0]}); + BOOST_ASSERT(chainActive.Height() == prevHeight+1); + + // another disabling spork + GenerateBlock({sporkTx2}); + // ensure lelantus is disabled + prevHeight = chainActive.Height(); + GenerateBlock({sparkMints[1]}); + BOOST_ASSERT(chainActive.Height() == prevHeight); + + // block with enabling spork + GenerateBlock({sporkTx3}); + // ensure lelantus is enabled now + prevHeight = chainActive.Height(); + GenerateBlock({sparkMints[1]}); + BOOST_ASSERT(chainActive.Height() == prevHeight+1); +} + +BOOST_AUTO_TEST_CASE(mempool) +{ + int prevHeight; + pwalletMain->SetBroadcastTransactions(true); + + for (int n=chainActive.Height(); n<1000; n++) + GenerateBlock({}); + + auto utxos = BuildSimpleUtxoMap(coinbaseTxns); + CMutableTransaction sporkTx1 = CreateSporkTx(utxos, coinbaseKey, { + {CSporkAction::sporkDisable, CSporkAction::featureSpark, 0, 1075} + }); + CMutableTransaction sporkTx2 = CreateSporkTx(utxos, coinbaseKey, { + {CSporkAction::sporkDisable, CSporkAction::featureSpark, 0, 1085} + }); + + std::vector sparkMints; + GenerateMints({1*COIN, 2*COIN}, sparkMints); + ::mempool.removeRecursive(sparkMints[0]); + ::mempool.removeRecursive(sparkMints[1]); + + CBlock blockWithSparkMint = CreateBlock({sparkMints[0]}, coinbaseKey); + + // put one mint into the mempool + CommitToMempool(sparkMints[0]); + BOOST_ASSERT(::mempool.size() == 1); + + // push spork to mempool + CommitToMempool(sporkTx1); + // spork should be in the mempool, spark mint should be pushed out of it + BOOST_ASSERT(::mempool.size() == 1); + BOOST_ASSERT(::mempool.exists(sporkTx1.GetHash()) && !::mempool.exists(sparkMints[0].GetHash())); + + // another spark tx shouldn't get to the mempool + CommitToMempool(sparkMints[1]); + BOOST_ASSERT(::mempool.size() == 1); + + // but should be accepted in block + prevHeight = chainActive.Height(); + ProcessNewBlock(Params(), std::make_shared(blockWithSparkMint), true, nullptr); + BOOST_ASSERT(chainActive.Height() == prevHeight+1); + + // mine spork into the block + CreateAndProcessBlock({sporkTx1}, coinbaseKey); + // mempool should clear + BOOST_ASSERT(::mempool.size() == 0); + + // because there is active spork at the tip spark mint shouldn't get into the mempool + BOOST_ASSERT(!CommitToMempool(sparkMints[1])); + + for (int n=chainActive.Height(); n<1075; n++) + CreateAndProcessBlock({}, coinbaseKey); + + // spork expired, should accept now + BOOST_ASSERT(CommitToMempool(sparkMints[1])); + // try and generate a block with second spork without it ever entering the mempool + CreateAndProcessBlock({sporkTx2}, coinbaseKey); + // now we have a mint in the mempool and active spork. Verify that miner correctly blocks the mint + // from being mined + fAllowMempoolTxsInCreateBlock = true; + CBlock block = CreateBlock({}, coinbaseKey); + for (CTransactionRef tx: block.vtx) { + BOOST_ASSERT(!tx->IsSparkTransaction()); + } + BOOST_ASSERT(::mempool.exists(sparkMints[1].GetHash())); + prevHeight = chainActive.Height(); + ProcessNewBlock(Params(), std::make_shared(block), true, nullptr); + BOOST_CHECK_EQUAL(chainActive.Height(), prevHeight+1); +} + +BOOST_AUTO_TEST_CASE(limit) +{ + int prevHeight; + pwalletMain->SetBroadcastTransactions(true); + + for (int n=chainActive.Height(); n<1000; n++) + GenerateBlock({}); + + auto utxos = BuildSimpleUtxoMap(coinbaseTxns); + CMutableTransaction sporkTx1 = CreateSporkTx(utxos, coinbaseKey, { + {CSporkAction::sporkLimit, CSporkAction::featureSparkTransparentLimit, 100*COIN, 1050} + }); + + auto params = spark::Params::get_default(); + + // Generate keys + const spark::SpendKey spend_key(params); + const spark::FullViewKey full_view_key(spend_key); + const spark::IncomingViewKey incoming_view_key(full_view_key); + + // Generate address + const spark::Address address(incoming_view_key, 12345); + + std::vector sparkMints; + for (int i=0; i<10; i++) { + std::vector> wtxAndFee; + std::vector mints{{address, 50*COIN, ""}}; + std::string error = pwalletMain->MintAndStoreSpark(mints, wtxAndFee, false); + BOOST_ASSERT(error.empty()); + for (auto &w: wtxAndFee) + sparkMints.emplace_back(*w.first.tx); + } + + GenerateBlock(sparkMints); + + for (int i=0; i<10; i++) + GenerateBlock({}); + + CAmount fee = 0; + CWalletTx spendWalletTx = pwalletMain->SpendAndStoreSpark({{script, 120*COIN, false, ""}}, {}, fee); + + CMutableTransaction spendTx = *spendWalletTx.tx; + + ::mempool.removeRecursive(spendWalletTx); + + auto sparkSpend = spark::ParseSparkSpend(spendTx); + std::vector lTags = sparkSpend.getUsedLTags(); + + // generate two smaller spark spend txs + CWalletTx smallSparkWalletTxs[2] = { + pwalletMain->SpendAndStoreSpark({{script, 70*COIN, false, ""}}, {}, fee), + pwalletMain->SpendAndStoreSpark({{script, 70*COIN, false, ""}}, {}, fee), + }; + + CMutableTransaction smallSparkTxs[2] = {*smallSparkWalletTxs[0].tx, *smallSparkWalletTxs[1].tx}; + + CommitToMempool(sporkTx1); + BOOST_ASSERT(::mempool.size() == 3); // two small spark spends and spork + + fAllowMempoolTxsInCreateBlock = true; + CBlock block = CreateBlock({}, script); + // should only have one spark spend transaction in the block + int nSparkSpends = 0; + for (CTransactionRef ptx: block.vtx) { + if (ptx->IsSparkSpend()) + nSparkSpends++; + } + BOOST_ASSERT(nSparkSpends == 1); + prevHeight = chainActive.Height(); + ProcessNewBlock(Params(), std::make_shared(block), true, nullptr); + BOOST_ASSERT(chainActive.Height() == prevHeight+1); + // one spark spend should be left at the mempool + BOOST_ASSERT(::mempool.size() == 1); + + // mine remaining spark spend into the block + prevHeight = chainActive.Height(); + GenerateBlock({}); + BOOST_ASSERT(chainActive.Height() == prevHeight+1); + BOOST_ASSERT(::mempool.size() == 0); + fAllowMempoolTxsInCreateBlock = false; + + // large spark spend tx is out of range, should fail now + BOOST_ASSERT(!CommitToMempool(spendTx)); + // should fail in block as well + prevHeight = chainActive.Height(); + GenerateBlock({spendTx}); + BOOST_ASSERT(chainActive.Height() == prevHeight); + + // skip to 1050 (spork expiration block) + for (int n=chainActive.Height(); n<1050; n++) + GenerateBlock({}); + + // should be accepted into the mempool + BOOST_ASSERT(CommitToMempool(spendTx)); + // and be mined into the block + prevHeight = chainActive.Height(); + GenerateBlock({spendTx}); + BOOST_ASSERT(chainActive.Height() == prevHeight+1); + // mempool should be clear + BOOST_ASSERT(::mempool.size() == 0); + // lTags should go into the state + spark::CSparkState *sparkState = spark::CSparkState::GetState(); + for (const GroupElement &lTag : lTags) + BOOST_ASSERT(sparkState->IsUsedLTag(lTag)); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/test_bitcoin.cpp b/src/test/test_bitcoin.cpp index 72ed006b2b..870d1be3ae 100644 --- a/src/test/test_bitcoin.cpp +++ b/src/test/test_bitcoin.cpp @@ -186,10 +186,12 @@ CBlock TestChain100Setup::CreateBlock(const std::vector& tx } // Replace mempool-selected txns with just coinbase plus passed-in txns: - if (!fAllowMempoolTxsInCreateBlock) + if (!fAllowMempoolTxsInCreateBlock) { block.vtx.resize(1); - // Re-add quorum commitments - block.vtx.insert(block.vtx.end(), llmqCommitments.begin(), llmqCommitments.end()); + // Re-add quorum commitments + block.vtx.insert(block.vtx.end(), llmqCommitments.begin(), llmqCommitments.end()); + } + BOOST_FOREACH(const CMutableTransaction& tx, txns) block.vtx.push_back(MakeTransactionRef(tx)); From efebbcc4f5bb17b4657fe9e908c6556e91fc97ef Mon Sep 17 00:00:00 2001 From: Peter Shugalev Date: Mon, 22 Jan 2024 09:54:27 +0100 Subject: [PATCH 4/6] Fixes for spark limit block template creation --- src/miner.cpp | 63 +++++++++++++++++++++++-------------- src/miner.h | 5 +++ src/test/evospork_tests.cpp | 9 ++---- 3 files changed, 46 insertions(+), 31 deletions(-) diff --git a/src/miner.cpp b/src/miner.cpp index fee39aaff0..492cb3477b 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -1003,34 +1003,49 @@ void BlockAssembler::FillBlackListForBlockTemplate() { // Now if we have limit on lelantus transparent outputs scan mempool and drop all the transactions exceeding the limit if (sporkMap.count(CSporkAction::featureLelantusTransparentLimit) > 0) { - CAmount limit = sporkMap[CSporkAction::featureLelantusTransparentLimit].second; + BlacklistTxsExceedingLimit(sporkMap[CSporkAction::featureLelantusTransparentLimit].second, + [](const CTransaction &tx)->bool { return tx.IsLelantusJoinSplit(); }, + [](const CTransaction &tx)->CAmount { return lelantus::GetSpendTransparentAmount(tx); }); + } - std::vector joinSplitTxs; - for (CTxMemPool::txiter mi = mempool.mapTx.begin(); mi != mempool.mapTx.end(); ++mi) { - if (txBlackList.count(mi) == 0 && mi->GetTx().IsLelantusJoinSplit()) - joinSplitTxs.push_back(mi); - } + // Same for spark spends + if (sporkMap.count(CSporkAction::featureSparkTransparentLimit) > 0) { + BlacklistTxsExceedingLimit(sporkMap[CSporkAction::featureSparkTransparentLimit].second, + [](const CTransaction &tx)->bool { return tx.IsSparkSpend(); }, + [](const CTransaction &tx)->CAmount { return spark::GetSpendTransparentAmount(tx); }); + } +} - // sort join splits in order of their transparent outputs so large txs won't block smaller ones - // from getting into the mempool - std::sort(joinSplitTxs.begin(), joinSplitTxs.end(), - [](CTxMemPool::txiter a, CTxMemPool::txiter b) -> bool { - return lelantus::GetSpendTransparentAmount(a->GetTx()) < lelantus::GetSpendTransparentAmount(b->GetTx()); - }); - - CAmount transparentAmount = 0; - std::vector::const_iterator it; - for (it = joinSplitTxs.cbegin(); it != joinSplitTxs.cend(); ++it) { - CAmount output = lelantus::GetSpendTransparentAmount((*it)->GetTx()); - if (transparentAmount + output > limit) - break; - transparentAmount += output; - } +void BlockAssembler::BlacklistTxsExceedingLimit(CAmount limit, + std::function txTypeFilter, + std::function txAmount) { + + std::vector txList; + for (CTxMemPool::txiter mi = mempool.mapTx.begin(); mi != mempool.mapTx.end(); ++mi) { + if (txBlackList.count(mi) == 0 && txTypeFilter(mi->GetTx())) + txList.push_back(mi); + } - // found all the joinsplit transaction fitting in the limit, blacklist the rest - while (it != joinSplitTxs.cend()) - mempool.CalculateDescendants(*it++, txBlackList); + // sort transactions in order of their transparent outputs so large txs won't block smaller ones + // from getting into the mempool + std::sort(txList.begin(), txList.end(), + [=](CTxMemPool::txiter a, CTxMemPool::txiter b) -> bool { + return txAmount(a->GetTx()) < txAmount(b->GetTx()); + }); + + CAmount transparentAmount = 0; + std::vector::const_iterator it; + for (it = txList.cbegin(); it != txList.cend(); ++it) { + CAmount output = txAmount((*it)->GetTx()); + if (transparentAmount + output > limit) + break; + transparentAmount += output; } + + // found all the private transaction fitting in the limit, blacklist the rest + while (it != txList.cend()) + mempool.CalculateDescendants(*it++, txBlackList); + } ////////////////////////////////////////////////////////////////////////////// diff --git a/src/miner.h b/src/miner.h index b6348ab94e..cd55d640eb 100644 --- a/src/miner.h +++ b/src/miner.h @@ -232,6 +232,11 @@ class BlockAssembler /** Fill txBlackList set */ void FillBlackListForBlockTemplate(); + + /** Ensure spark/lelantus txs don't exceed specific limit */ + void BlacklistTxsExceedingLimit(CAmount limit, + std::function txTypeFilter, + std::function txAmount); }; /** Modify the extranonce in a block */ diff --git a/src/test/evospork_tests.cpp b/src/test/evospork_tests.cpp index 9fe24e15b6..11d29a8ef2 100644 --- a/src/test/evospork_tests.cpp +++ b/src/test/evospork_tests.cpp @@ -593,13 +593,8 @@ BOOST_AUTO_TEST_CASE(limit) auto params = spark::Params::get_default(); - // Generate keys - const spark::SpendKey spend_key(params); - const spark::FullViewKey full_view_key(spend_key); - const spark::IncomingViewKey incoming_view_key(full_view_key); - - // Generate address - const spark::Address address(incoming_view_key, 12345); + BOOST_ASSERT(pwalletMain->sparkWallet); + spark::Address address = pwalletMain->sparkWallet->generateNewAddress(); std::vector sparkMints; for (int i=0; i<10; i++) { From fc496f4f0a974522d561f900d797611b552a5acd Mon Sep 17 00:00:00 2001 From: Peter Shugalev Date: Mon, 22 Jan 2024 13:04:34 +0100 Subject: [PATCH 5/6] Fixed test instability --- src/test/evospork_tests.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/evospork_tests.cpp b/src/test/evospork_tests.cpp index 11d29a8ef2..49a08fab0e 100644 --- a/src/test/evospork_tests.cpp +++ b/src/test/evospork_tests.cpp @@ -448,6 +448,7 @@ struct SparkSporkTestingSetup : public SparkTestingSetup ~SparkSporkTestingSetup() { mutableParams = originalParams; + spark::CSparkState::GetState()->Reset(); } }; From ec1b54e8d5ed00fca605779b32c359e13d44c9e6 Mon Sep 17 00:00:00 2001 From: Peter Shugalev Date: Mon, 22 Jan 2024 16:17:02 +0100 Subject: [PATCH 6/6] Additional cleanup in tests --- src/test/evospork_tests.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/test/evospork_tests.cpp b/src/test/evospork_tests.cpp index 49a08fab0e..fb01ac97bc 100644 --- a/src/test/evospork_tests.cpp +++ b/src/test/evospork_tests.cpp @@ -442,6 +442,8 @@ struct SparkSporkTestingSetup : public SparkTestingSetup SparkSporkTestingSetup() : SparkTestingSetup(), mutableParams(const_cast(Params().GetConsensus())) { + spark::CSparkState::GetState()->Reset(); + mempool.clear(); originalParams = mutableParams; mutableParams.nEvoSporkStopBlock = 2000; } @@ -449,6 +451,7 @@ struct SparkSporkTestingSetup : public SparkTestingSetup ~SparkSporkTestingSetup() { mutableParams = originalParams; spark::CSparkState::GetState()->Reset(); + mempool.clear(); } };