From d7c66eda7ebef8c4f33ef4a277f55b4e464adc5b Mon Sep 17 00:00:00 2001 From: Alessandro Rezzi Date: Wed, 26 Jul 2023 14:18:57 +0200 Subject: [PATCH] Unit tests --- src/wallet/test/pos_validations_tests.cpp | 354 ++++++++++++++++++++-- 1 file changed, 331 insertions(+), 23 deletions(-) diff --git a/src/wallet/test/pos_validations_tests.cpp b/src/wallet/test/pos_validations_tests.cpp index 41f96b77d04b8f..f34439bf679b32 100644 --- a/src/wallet/test/pos_validations_tests.cpp +++ b/src/wallet/test/pos_validations_tests.cpp @@ -2,19 +2,30 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or https://www.opensource.org/licenses/mit-license.php. +#include "amount.h" +#include "optional.h" +#include "primitives/transaction.h" +#include "sapling/address.h" +#include "sapling/zip32.h" +#include "sync.h" +#include "validation.h" #include "wallet/test/pos_test_fixture.h" #include "blockassembler.h" -#include "coincontrol.h" -#include "util/blockstatecatcher.h" #include "blocksignature.h" +#include "coincontrol.h" #include "consensus/merkle.h" #include "primitives/block.h" +#include "sapling/sapling_operation.h" #include "script/sign.h" #include "test/util/blocksutil.h" +#include "util/blockstatecatcher.h" #include "wallet/wallet.h" #include +#include +#include +#include BOOST_AUTO_TEST_SUITE(pos_validations_tests) @@ -143,29 +154,36 @@ static bool IsSpentOnFork(const COutput& coin, std::initializer_list CreateBlockInternal(CWallet* pwalletMain, const std::vector& txns = {}, - CBlockIndex* customPrevBlock = nullptr, - std::initializer_list> forkchain = {}) +std::shared_ptr CreateBlockInternal(CWallet* pwalletMain, const std::vector& txns = {}, CBlockIndex* customPrevBlock = nullptr, std::initializer_list> forkchain = {}, bool fNoMempoolTx = true, bool isShieldStake = false) { - std::vector availableUTXOs; - BOOST_CHECK(pwalletMain->StakeableUTXOs(&availableUTXOs)); + std::vector> availableCoins; + if (!isShieldStake) { + std::vector availableUTXOs; + BOOST_CHECK(pwalletMain->StakeableUTXOs(&availableUTXOs)); + + // Remove any utxo which is not deeper than 120 blocks (for the same reasoning + // used when selecting tx inputs in CreateAndCommitTx) + // Also, as the wallet is not prepared to follow several chains at the same time, + // need to manually remove from the stakeable utxo set every already used + // coinstake inputs on the previous blocks of the parallel chain so they + // are not used again. + for (auto it = availableUTXOs.begin(); it != availableUTXOs.end();) { + if (it->nDepth <= 120 || IsSpentOnFork(*it, forkchain)) { + it = availableUTXOs.erase(it); + } else { + it++; + } + } - // Remove any utxo which is not deeper than 120 blocks (for the same reasoning - // used when selecting tx inputs in CreateAndCommitTx) - // Also, as the wallet is not prepared to follow several chains at the same time, - // need to manually remove from the stakeable utxo set every already used - // coinstake inputs on the previous blocks of the parallel chain so they - // are not used again. - for (auto it = availableUTXOs.begin(); it != availableUTXOs.end();) { - if (it->nDepth <= 120 || IsSpentOnFork(*it, forkchain)) { - it = availableUTXOs.erase(it); - } else { - it++; + for (auto& utxo : availableUTXOs) { + availableCoins.push_back(std::make_unique(utxo)); + } + } else { + std::vector availableNotes; + BOOST_CHECK(pwalletMain->StakeableNotes(&availableNotes)); + for (auto& note : availableNotes) { + availableCoins.push_back(std::make_unique(note)); } - } - std::vector> availableCoins; - for (auto& utxo : availableUTXOs) { - availableCoins.push_back(std::make_unique(utxo)); } std::unique_ptr pblocktemplate = BlockAssembler( Params(), false) @@ -173,17 +191,20 @@ std::shared_ptr CreateBlockInternal(CWallet* pwalletMain, const std::vec pwalletMain, true, availableCoins, - true, + fNoMempoolTx, false, customPrevBlock, false); BOOST_ASSERT(pblocktemplate); auto pblock = std::make_shared(pblocktemplate->block); if (!txns.empty()) { + if (isShieldStake) BOOST_CHECK(false); for (const auto& tx : txns) { pblock->vtx.emplace_back(MakeTransactionRef(tx)); } pblock->hashMerkleRoot = BlockMerkleRoot(*pblock); + const int nHeight = (customPrevBlock != nullptr ? customPrevBlock->nHeight + 1 : WITH_LOCK(cs_main, return chainActive.Height()) + 1); + pblock->hashFinalSaplingRoot = CalculateSaplingTreeRoot(&*pblock, nHeight, Params()); assert(SignBlock(*pblock, *pwalletMain)); } return pblock; @@ -447,4 +468,291 @@ BOOST_FIXTURE_TEST_CASE(created_on_fork_tests, TestPoSChainSetup) BOOST_CHECK(ProcessNewBlock(pblockI, nullptr)); } +// From now on SHIELD STAKE TESTS +static void ActivateShieldStaking(CWallet* pwalletMain) +{ + while (WITH_LOCK(cs_main, return chainActive.Tip()->nHeight) < 600) { + std::shared_ptr pblock = CreateBlockInternal(pwalletMain); + ProcessNewBlock(pblock, nullptr); + } +} + +static void UpdateAndProcessShieldStakeBlock(std::shared_ptr pblock, CWallet* pwalletMain, std::string processError, int expBlock) +{ + pblock->hashMerkleRoot = BlockMerkleRoot(*pblock); + pblock->hashFinalSaplingRoot = CalculateSaplingTreeRoot(&*pblock, WITH_LOCK(cs_main, return chainActive.Height()) + 1, Params()); + if (pblock->IsProofOfShieldStake()) BOOST_CHECK(SignBlock(*pblock, *pwalletMain, pblock->vtx[1]->shieldStakeRandomness, pblock->vtx[1]->shieldStakePrivKey)); + ProcessBlockAndCheckRejectionReason(pblock, processError, expBlock); +} + +// Create a coinshieldstake with an eventual addition of unwanted pieces +// Entries of shieldNotes are the notes you are going to spend inside the coinshieldstake +static bool CreateFakeShieldReward(CWallet* pwalletMain, const std::vector& shieldNotes, CMutableTransaction& txNew, int deltaReward, bool splitOutput, bool addMultiEmptyOutput) +{ + int nHeight = WITH_LOCK(cs_main, return chainActive.Tip()->nHeight); + CAmount nMasternodePayment = GetMasternodePayment(nHeight); + TransactionBuilder txBuilder(Params().GetConsensus(), pwalletMain); + txBuilder.SetFee(0); + txBuilder.AddStakeInput(); + if (addMultiEmptyOutput) { + txBuilder.AddStakeInput(); + } + CAmount val = GetBlockValue(nHeight) - nMasternodePayment + deltaReward * COIN; + for (const CStakeableShieldNote& note : shieldNotes) { + val += note.note.value(); + } + val /= (splitOutput + 1); + for (int i = 0; i < (splitOutput + 1); i++) { + txBuilder.AddSaplingOutput(pwalletMain->GetSaplingScriptPubKeyMan()->getCommonOVK(), pwalletMain->GenerateNewSaplingZKey(), val); + } + + std::vector sk; + for (const CStakeableShieldNote& note : shieldNotes) { + libzcash::SaplingExtendedSpendingKey t; + if (!pwalletMain->GetSaplingExtendedSpendingKey(note.address, t)) { + return false; + } + sk.push_back(t); + } + + uint256 anchor; + std::vector> witnesses; + std::vector noteop; + for (const CStakeableShieldNote& note : shieldNotes) { + noteop.emplace_back(note.op); + } + + pwalletMain->GetSaplingScriptPubKeyMan()->GetSaplingNoteWitnesses(noteop, witnesses, anchor); + int i = 0; + for (const CStakeableShieldNote& note : shieldNotes) { + txBuilder.AddSaplingSpend(sk[i].expsk, note.note, anchor, witnesses[i].get()); + i++; + } + + const auto& txTrial = txBuilder.Build().GetTx(); + if (txTrial) { + txNew = CMutableTransaction(*txTrial); + txNew.shieldStakePrivKey = sk[0].expsk.ask; + txNew.shieldStakeRandomness = txBuilder.GetShieldStakeRandomness(); + return true; + } else { + return false; + } +} + +// Create a sapling operation that can build a shield tx +static SaplingOperation CreateOperationAndBuildTx(std::unique_ptr& pwallet, + CAmount amount, + bool selectTransparentCoins) +{ + // Create the operation + libzcash::SaplingPaymentAddress pa = pwallet->GenerateNewSaplingZKey("s1"); + std::vector recipients; + recipients.emplace_back(pa, amount, "", false); + SaplingOperation operation(Params().GetConsensus(), pwallet.get()); + operation.setMinDepth(1); + auto operationResult = operation.setRecipients(recipients) + ->setSelectTransparentCoins(selectTransparentCoins) + ->setSelectShieldedCoins(!selectTransparentCoins) + ->build(); + BOOST_ASSERT_MSG(operationResult, operationResult.getError().c_str()); + + CValidationState state; + BOOST_ASSERT_MSG( + CheckTransaction(operation.getFinalTx(), state, true), + "Invalid Sapling transaction"); + return operation; +} + +// The aim of this test is verifying some basic rules regarding the coinshieldstake, +// double spend and non malleability of the shield staked block +BOOST_FIXTURE_TEST_CASE(coinshieldstake_tests, TestPoSChainSetup) +{ + // Verify that we are at block 250 and then activate shield Staking + BOOST_CHECK_EQUAL(WITH_LOCK(cs_main, return chainActive.Tip()->nHeight), 250); + SyncWithValidationInterfaceQueue(); + ActivateShieldStaking(pwalletMain.get()); + + // Create two sapling notes with 10k PIVs + { + CReserveKey reservekey(&*pwalletMain); + for (int i = 0; i < 2; i++) { + SaplingOperation operation = CreateOperationAndBuildTx(pwalletMain, 10000 * COIN, true); + pwalletMain->CommitTransaction(operation.getFinalTxRef(), reservekey, nullptr); + } + std::shared_ptr pblock = CreateBlockInternal(pwalletMain.get(), {}, nullptr, {}, false); + BOOST_CHECK(ProcessNewBlock(pblock, nullptr)); + + // Sanity check on the block created + BOOST_CHECK(pblock->vtx.size() == 4); + BOOST_CHECK(pblock->vtx[2]->sapData->vShieldedOutput.size() == 1 && pblock->vtx[3]->sapData->vShieldedOutput.size() == 1); + } + + // Create 20 more blocks, in such a way that the sapling notes will be stakeable + for (int i = 0; i < 20; i++) { + std::shared_ptr pblock = CreateBlockInternal(pwalletMain.get(), {}, nullptr, {}, true); + ProcessNewBlock(pblock, nullptr); + } + + // Create a shield stake block + std::shared_ptr pblock = CreateBlockInternal(pwalletMain.get(), {}, nullptr, {}, false, true); + BOOST_CHECK(pblock->IsProofOfShieldStake()); + + // And let's begin with tests: + // 1) ShieldStake blocks are not malleable, for example let's try to add a new tx + { + std::shared_ptr pblockA = std::make_shared(*pblock); + auto cTx = CreateAndCommitTx(pwalletMain.get(), *pwalletMain->getNewAddress("").getObjResult(), 249 * COIN); + pblockA->vtx.emplace_back(MakeTransactionRef(cTx)); + pblockA->hashMerkleRoot = BlockMerkleRoot(*pblockA); + pblockA->hashFinalSaplingRoot = CalculateSaplingTreeRoot(&*pblockA, WITH_LOCK(cs_main, return chainActive.Height()) + 1, Params()); + ProcessBlockAndCheckRejectionReason(pblockA, "bad-PoS-sig", 621); + } + + // 2) The Note used to ShieldStake cannot be spent two times in the same block: + { + std::shared_ptr pblockB = std::make_shared(*pblock); + uint256 shieldStakeNullifier = pblock.get()->vtx[1]->sapData->vShieldedSpend[0].nullifier; + + // Build a random shield tx that spends the shield stake note. + SaplingOperation operation = CreateOperationAndBuildTx(pwalletMain, 5 * COIN, false); + // Sanity check on the note that was spent + BOOST_CHECK(operation.getFinalTx().sapData->vShieldedSpend[0].nullifier == shieldStakeNullifier); + BOOST_CHECK(operation.getFinalTx().sapData->vShieldedSpend.size() == 1); + + // Update the block, resign and try to process + pblockB->vtx.emplace_back(operation.getFinalTxRef()); + UpdateAndProcessShieldStakeBlock(pblockB, &*pwalletMain, "bad-txns-sapling-requirements-not-met", 621); + } + // 3) Let's try to change the structure of the CoinShieldStake tx: + { + std::shared_ptr pblockC = std::make_shared(*pblock); + uint256 shieldStakeNullifier = pblock.get()->vtx[1]->sapData->vShieldedSpend[0].nullifier; + std::vector stakeableNotes = {}; + BOOST_CHECK(pwalletMain->StakeableNotes(&stakeableNotes)); + // Usual sanity check + BOOST_CHECK(stakeableNotes.size() == 2); + int shieldNoteIndex = stakeableNotes[0].nullifier == shieldStakeNullifier ? 0 : 1; + int otherNoteIndex = (1 + shieldNoteIndex) % 2; + + // 3.1) Staker is trying to get paid a different amount from the expected + CMutableTransaction mtx; + BOOST_CHECK(CreateFakeShieldReward(&*pwalletMain, {stakeableNotes[shieldNoteIndex]}, mtx, 100000, false, false)); + pblockC->vtx[1] = MakeTransactionRef(mtx); + UpdateAndProcessShieldStakeBlock(pblockC, &*pwalletMain, "bad-blk-amount", 621); + + // 3.2) Staker is trying to add more than one empty vout + BOOST_CHECK(CreateFakeShieldReward(&*pwalletMain, {stakeableNotes[shieldNoteIndex]}, mtx, 0, false, true)); + pblockC->vtx[1] = MakeTransactionRef(mtx); + // Why this processError? Well the mtx is not coinstake since it has saplingdata and the mtx is not coinshieldstake since the vout has length different than 1, + // therefore the block is not proof of stake => invalid PoW + UpdateAndProcessShieldStakeBlock(pblockC, &*pwalletMain, "PoW-ended", 621); + + // 3.3) Staker is trying to add another shield input + BOOST_CHECK(CreateFakeShieldReward(&*pwalletMain, {stakeableNotes[shieldNoteIndex], stakeableNotes[otherNoteIndex]}, mtx, 0, false, false)); + pblockC->vtx[1] = MakeTransactionRef(mtx); + UpdateAndProcessShieldStakeBlock(pblockC, &*pwalletMain, "bad-scs-multi-inputs", 621); + + // 3.4) TODO: add an upper bound on the maximum number of shield outputs (or a malicious node could create huge blocks without paying fees)?? + } + // 4) TODO: test what happens is the proof is faked + + // 5) Last but not least, the original block is processed without any errors. + BOOST_CHECK(ProcessNewBlock(pblock, nullptr)); +} + +// The aim of this test is verifying that shield stake rewards can be spent only as intended. +BOOST_FIXTURE_TEST_CASE(shieldstake_fork_tests, TestPoSChainSetup) +{ + /* + Consider the following chain diagram: + A -- B -- C -- D -- E -- F + \ + -- D1 -- E1 --F1 -- G1 + + I will verify that a shield stake reward created in D can be spent in F but not in the fork block E1 + */ + // Verify that we are at block 250 and then activate shield Staking + BOOST_CHECK_EQUAL(WITH_LOCK(cs_main, return chainActive.Tip()->nHeight), 250); + SyncWithValidationInterfaceQueue(); + ActivateShieldStaking(pwalletMain.get()); + // Create a sapling notes of 10k PIVs + { + CReserveKey reservekey(&*pwalletMain); + SaplingOperation operation = CreateOperationAndBuildTx(pwalletMain, 10000 * COIN, true); + pwalletMain->CommitTransaction(operation.getFinalTxRef(), reservekey, nullptr); + + std::shared_ptr pblock = CreateBlockInternal(pwalletMain.get(), {}, nullptr, {}, false); + BOOST_CHECK(ProcessNewBlock(pblock, nullptr)); + + // Sanity check on the block created + BOOST_CHECK(pblock->vtx.size() == 3); + BOOST_CHECK(pblock->vtx[2]->sapData->vShieldedOutput.size() == 1); + } + + // Create 20 more blocks, in such a way that the sapling notes will be stakeable, the 20-th is block C + for (int i = 0; i < 19; i++) { + std::shared_ptr pblock = CreateBlockInternal(pwalletMain.get(), {}, nullptr, {}, true); + ProcessNewBlock(pblock, nullptr); + } + std::shared_ptr pblockC = CreateBlockInternal(pwalletMain.get()); + BOOST_CHECK(ProcessNewBlock(pblockC, nullptr)); + + // Create a shielded pos block D + std::shared_ptr pblockD = CreateBlockInternal(pwalletMain.get(), {}, nullptr, {}, true, true); + + // Create D1 forked block that connects a new tx + std::shared_ptr pblockD1 = CreateBlockInternal(pwalletMain.get()); + + // Process blocks D and D1 + ProcessNewBlock(pblockD, nullptr); + ProcessNewBlock(pblockD1, nullptr); + BOOST_CHECK(WITH_LOCK(cs_main, return chainActive.Tip()->GetBlockHash() == pblockD->GetHash())); + + // Create block E + std::shared_ptr pblockE = CreateBlockInternal(pwalletMain.get(), {}, {}); + BOOST_CHECK(ProcessNewBlock(pblockE, nullptr)); + + // Verify that we indeed have the shield stake reward: + std::vector notes = {}; + BOOST_CHECK(pwalletMain->GetSaplingScriptPubKeyMan()->GetStakeableNotes(¬es, 1)); + BOOST_CHECK(notes.size() == 1); + BOOST_CHECK(notes[0].note.value() == 10004 * COIN); + uint256 rewardNullifier = notes[0].nullifier; + + // Build and commit a tx that spends the reward and check that it indeed spends it + auto operation = CreateOperationAndBuildTx(pwalletMain, 100 * COIN, false); + auto txRef = operation.getFinalTx(); + BOOST_CHECK(txRef.sapData->vShieldedSpend.size() == 1); + BOOST_CHECK(txRef.sapData->vShieldedSpend[0].nullifier == rewardNullifier); + std::string txHash; + operation.send(txHash); + + // Create the forked block E1 that spends the shield stake reward + // There is a little catch here: the validation checks for this kind of invalid block (in this case invalid anchor since the note does not exist on the forked chain) + // is done only on ConnectBlock which is called only when we are connecting the new block to the current chaintip + // now, since the fork is not the current active chain ConnectBlock is not called and the block seems to be valid. + std::shared_ptr pblockE1 = CreateBlockInternal(pwalletMain.get(), {txRef}, mapBlockIndex.at(pblockD1->GetHash()), {pblockD1}); + BOOST_CHECK(pblockE1->vtx[2]->sapData->vShieldedSpend.size() == 1); + BOOST_CHECK(pblockE1->vtx[2]->sapData->vShieldedSpend[0].nullifier == rewardNullifier); + BOOST_CHECK(ProcessNewBlock(pblockE1, nullptr)); + + // So how to see that the forked chain is indeed invalid? + // We can keep adding blocks to the forked chain until it becomes the active chain! + // In that moment connectblock will be called and the forked chain will become invalid. + std::shared_ptr pblockF1 = CreateBlockInternal(pwalletMain.get(), {}, mapBlockIndex.at(pblockE1->GetHash()), {pblockD1, pblockE1}); + BOOST_CHECK(ProcessNewBlock(pblockF1, nullptr)); + + // finally the wallet will try to activate the forkedchain and will figure out that we have a bad previous block + std::shared_ptr pblockG1 = CreateBlockInternal(pwalletMain.get(), {}, mapBlockIndex.at(pblockF1->GetHash()), {pblockD1, pblockE1, pblockF1}); + ProcessBlockAndCheckRejectionReason(pblockG1, "bad-prevblk", 623); + // Side note: Of course what I just showed doesn't depend on shield staking and will happen for any non-valid shield transaction sent to a forked chain, + // TODO: change the validation in such a way that the anchor and duplicated nullifiers are checked not only when the block is connected? + + // Finally let's see that the shield reward can be spent on the main chain: + BOOST_CHECK(WITH_LOCK(cs_main, return chainActive.Tip()->GetBlockHash() == pblockE->GetHash())); + std::shared_ptr pblockF = CreateBlockInternal(pwalletMain.get(), {txRef}); + BOOST_CHECK(ProcessNewBlock(pblockF, nullptr)); +} + BOOST_AUTO_TEST_SUITE_END()