diff --git a/.cargo/config.offline b/.cargo/config.offline index e8370b7666c71..38afbe560888b 100644 --- a/.cargo/config.offline +++ b/.cargo/config.offline @@ -1,9 +1,9 @@ [source.crates-io] replace-with = "vendored-sources" -[source."git+https://github.com/PIVX-Project/librustzcash?rev=e7662b23d16c38595ef9ad1f3ae683b1f54dc732"] +[source."git+https://github.com/PIVX-Project/librustzcash?rev=4822cf07b18d8726e9b3253a15f9e08d81752746"] git = "https://github.com/PIVX-Project/librustzcash" -rev = "e7662b23d16c38595ef9ad1f3ae683b1f54dc732" +rev = "4822cf07b18d8726e9b3253a15f9e08d81752746" replace-with = "vendored-sources" [source.vendored-sources] diff --git a/Cargo.lock b/Cargo.lock index fd6f90e1f59c7..f633b6046691e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -108,9 +108,9 @@ dependencies = [ [[package]] name = "blake2b_simd" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c2f0dc9a68c6317d884f97cc36cf5a3d20ba14ce404227df55e1af708ab04bc" +checksum = "23285ad32269793932e830392f2fe2f83e26488fd3ec778883a93c8323735780" dependencies = [ "arrayref", "arrayvec", @@ -119,9 +119,9 @@ dependencies = [ [[package]] name = "blake2s_simd" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6637f448b9e61dfadbdcbae9a885fadee1f3eaffb1f8d3c1965d3ade8bdfd44f" +checksum = "94230421e395b9920d23df13ea5d77a20e1725331f90fbbf6df6040b33f756ae" dependencies = [ "arrayref", "arrayvec", @@ -226,9 +226,9 @@ dependencies = [ [[package]] name = "constant_time_eq" -version = "0.2.6" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a53c0a4d288377e7415b53dcfc3c04da5cdc2cc95c8d5ac178b58f0b861ad6" +checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" [[package]] name = "cpufeatures" @@ -342,7 +342,7 @@ checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "equihash" version = "0.2.0" -source = "git+https://github.com/PIVX-Project/librustzcash?rev=e7662b23d16c38595ef9ad1f3ae683b1f54dc732#e7662b23d16c38595ef9ad1f3ae683b1f54dc732" +source = "git+https://github.com/PIVX-Project/librustzcash?rev=4822cf07b18d8726e9b3253a15f9e08d81752746#4822cf07b18d8726e9b3253a15f9e08d81752746" dependencies = [ "blake2b_simd", "byteorder", @@ -351,7 +351,7 @@ dependencies = [ [[package]] name = "f4jumble" version = "0.1.0" -source = "git+https://github.com/PIVX-Project/librustzcash?rev=e7662b23d16c38595ef9ad1f3ae683b1f54dc732#e7662b23d16c38595ef9ad1f3ae683b1f54dc732" +source = "git+https://github.com/PIVX-Project/librustzcash?rev=4822cf07b18d8726e9b3253a15f9e08d81752746#4822cf07b18d8726e9b3253a15f9e08d81752746" dependencies = [ "blake2b_simd", ] @@ -481,9 +481,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.147" +version = "0.2.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" [[package]] name = "libm" @@ -511,9 +511,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.19" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "memoffset" @@ -538,9 +538,9 @@ checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" [[package]] name = "num-bigint" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" dependencies = [ "autocfg", "num-integer", @@ -639,9 +639,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "poly1305" @@ -662,18 +662,18 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.32" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -795,18 +795,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.183" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.183" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aafe972d60b0b9bee71a91b92fee2d4fb3c9d7e8f6b179aa99f27203d99a4816" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", @@ -851,9 +851,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "2.0.28" +version = "2.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" +checksum = "9caece70c63bfba29ec2fed841a09851b14a235c60010fa4de58089b6c025668" dependencies = [ "proc-macro2", "quote", @@ -868,18 +868,18 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "thiserror" -version = "1.0.44" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" +checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.44" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" +checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" dependencies = [ "proc-macro2", "quote", @@ -941,9 +941,9 @@ checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" [[package]] name = "unicode-ident" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" @@ -987,9 +987,9 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.48.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", @@ -1002,45 +1002,45 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_i686_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "wyz" @@ -1054,7 +1054,7 @@ dependencies = [ [[package]] name = "zcash_address" version = "0.2.0" -source = "git+https://github.com/PIVX-Project/librustzcash?rev=e7662b23d16c38595ef9ad1f3ae683b1f54dc732#e7662b23d16c38595ef9ad1f3ae683b1f54dc732" +source = "git+https://github.com/PIVX-Project/librustzcash?rev=4822cf07b18d8726e9b3253a15f9e08d81752746#4822cf07b18d8726e9b3253a15f9e08d81752746" dependencies = [ "bech32", "bs58", @@ -1065,7 +1065,7 @@ dependencies = [ [[package]] name = "zcash_encoding" version = "0.2.0" -source = "git+https://github.com/PIVX-Project/librustzcash?rev=e7662b23d16c38595ef9ad1f3ae683b1f54dc732#e7662b23d16c38595ef9ad1f3ae683b1f54dc732" +source = "git+https://github.com/PIVX-Project/librustzcash?rev=4822cf07b18d8726e9b3253a15f9e08d81752746#4822cf07b18d8726e9b3253a15f9e08d81752746" dependencies = [ "byteorder", "nonempty", @@ -1087,7 +1087,7 @@ dependencies = [ [[package]] name = "zcash_primitives" version = "0.11.0" -source = "git+https://github.com/PIVX-Project/librustzcash?rev=e7662b23d16c38595ef9ad1f3ae683b1f54dc732#e7662b23d16c38595ef9ad1f3ae683b1f54dc732" +source = "git+https://github.com/PIVX-Project/librustzcash?rev=4822cf07b18d8726e9b3253a15f9e08d81752746#4822cf07b18d8726e9b3253a15f9e08d81752746" dependencies = [ "aes", "bip0039", @@ -1118,7 +1118,7 @@ dependencies = [ [[package]] name = "zcash_proofs" version = "0.11.0" -source = "git+https://github.com/PIVX-Project/librustzcash?rev=e7662b23d16c38595ef9ad1f3ae683b1f54dc732#e7662b23d16c38595ef9ad1f3ae683b1f54dc732" +source = "git+https://github.com/PIVX-Project/librustzcash?rev=4822cf07b18d8726e9b3253a15f9e08d81752746#4822cf07b18d8726e9b3253a15f9e08d81752746" dependencies = [ "bellman", "blake2b_simd", diff --git a/Cargo.toml b/Cargo.toml index e6f4c4cc839fa..531c54bfb3990 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ byteorder = "1.4.3" group = "0.13" rand_core = "0.6.4" jubjub = "0.10.0" -zcash_primitives = { git="https://github.com/PIVX-Project/librustzcash", rev="e7662b23d16c38595ef9ad1f3ae683b1f54dc732" } -zcash_proofs = { git="https://github.com/PIVX-Project/librustzcash", rev="e7662b23d16c38595ef9ad1f3ae683b1f54dc732" } +zcash_primitives = { git="https://github.com/PIVX-Project/librustzcash", rev="4822cf07b18d8726e9b3253a15f9e08d81752746" } +zcash_proofs = { git="https://github.com/PIVX-Project/librustzcash", rev="4822cf07b18d8726e9b3253a15f9e08d81752746" } zcash_note_encryption = "0.3.0" diff --git a/src/blockassembler.cpp b/src/blockassembler.cpp index 94f64ea1925a8..1577f12c2a964 100644 --- a/src/blockassembler.cpp +++ b/src/blockassembler.cpp @@ -19,7 +19,9 @@ #include "policy/policy.h" #include "pow.h" #include "primitives/transaction.h" +#include "sapling/saplingscriptpubkeyman.h" #include "spork.h" +#include "stakeinput.h" #include "timedata.h" #include "util/system.h" #include "util/validation.h" @@ -66,8 +68,7 @@ static CMutableTransaction NewCoinbase(const int nHeight, const CScript* pScript return txCoinbase; } -bool SolveProofOfStake(CBlock* pblock, CBlockIndex* pindexPrev, CWallet* pwallet, - std::vector* availableCoins, bool stopPoSOnNewBlock) +bool SolveProofOfStake(CBlock* pblock, CBlockIndex* pindexPrev, CMutableTransaction& txCoinStake, CWallet* pwallet, const std::vector>& availableCoins, bool stopPoSOnNewBlock) { boost::this_thread::interruption_point(); @@ -77,15 +78,14 @@ bool SolveProofOfStake(CBlock* pblock, CBlockIndex* pindexPrev, CWallet* pwallet // Sync wallet before create coinstake pwallet->BlockUntilSyncedToCurrentChain(); - CMutableTransaction txCoinStake; int64_t nTxNewTime = 0; - if (!pwallet->CreateCoinStake(pindexPrev, - pblock->nBits, - txCoinStake, - nTxNewTime, - availableCoins, - stopPoSOnNewBlock - )) { + CStakeableInterface* pStake = pwallet->CreateCoinStake(*pindexPrev, + pblock->nBits, + txCoinStake, + nTxNewTime, + availableCoins, + stopPoSOnNewBlock); + if (!pStake) { LogPrint(BCLog::STAKING, "%s : stake not found\n", __func__); return false; } @@ -96,6 +96,7 @@ bool SolveProofOfStake(CBlock* pblock, CBlockIndex* pindexPrev, CWallet* pwallet FillBlockPayee(txCoinbase, txCoinStake, pindexPrev, true); // Sign coinstake + if (!pwallet->SignCoinStake(txCoinStake)) { const COutPoint& stakeIn = txCoinStake.vin[0].prevout; return error("Unable to sign coinstake with input %s-%d", stakeIn.hash.ToString(), stakeIn.n); @@ -104,6 +105,13 @@ bool SolveProofOfStake(CBlock* pblock, CBlockIndex* pindexPrev, CWallet* pwallet pblock->vtx.emplace_back(MakeTransactionRef(txCoinbase)); pblock->vtx.emplace_back(MakeTransactionRef(txCoinStake)); pblock->nTime = nTxNewTime; + + if (pblock->IsProofOfShieldStake()) { + auto& shieldStake = *static_cast(pStake); + if (!pwallet->GetSaplingScriptPubKeyMan()->ComputeShieldStakeProof(*pblock, shieldStake, shieldStake.suggestedValue)) { + return false; + } + } return true; } @@ -156,14 +164,14 @@ void BlockAssembler::resetBlock() } std::unique_ptr BlockAssembler::CreateNewBlock(const CScript& scriptPubKeyIn, - CWallet* pwallet, - bool fProofOfStake, - std::vector* availableCoins, - bool fNoMempoolTx, - bool fTestValidity, - CBlockIndex* prevBlock, - bool stopPoSOnNewBlock, - bool fIncludeQfc) + CWallet* pwallet, + bool fProofOfStake, + const std::vector>& availableCoins, + bool fNoMempoolTx, + bool fTestValidity, + CBlockIndex* prevBlock, + bool stopPoSOnNewBlock, + bool fIncludeQfc) { resetBlock(); @@ -186,9 +194,10 @@ std::unique_ptr BlockAssembler::CreateNewBlock(const CScript& sc pblock->nVersion = gArgs.GetArg("-blockversion", pblock->nVersion); } + CMutableTransaction txCoinStake; + // Depending on the tip height, try to find a coinstake who solves the block or create a coinbase tx. - if (!(fProofOfStake ? SolveProofOfStake(pblock, pindexPrev, pwallet, availableCoins, stopPoSOnNewBlock) - : CreateCoinbaseTx(pblock, scriptPubKeyIn, pindexPrev))) { + if (!(fProofOfStake ? SolveProofOfStake(pblock, pindexPrev, txCoinStake, pwallet, availableCoins, stopPoSOnNewBlock) : CreateCoinbaseTx(pblock, scriptPubKeyIn, pindexPrev))) { return nullptr; } @@ -238,7 +247,7 @@ std::unique_ptr BlockAssembler::CreateNewBlock(const CScript& sc if (fProofOfStake) { // this is only for PoS because the IncrementExtraNonce does it for PoW pblock->hashMerkleRoot = BlockMerkleRoot(*pblock); LogPrintf("CPUMiner : proof-of-stake block found %s \n", pblock->GetHash().GetHex()); - if (!SignBlock(*pblock, *pwallet)) { + if (!SignBlock(*pblock, *pwallet, txCoinStake.shieldStakeRandomness, txCoinStake.shieldStakePrivKey)) { LogPrintf("%s: Signing new block with UTXO key failed \n", __func__); return nullptr; } diff --git a/src/blockassembler.h b/src/blockassembler.h index 0db38ebd3f68b..59d861b80858d 100644 --- a/src/blockassembler.h +++ b/src/blockassembler.h @@ -8,12 +8,15 @@ #define PIVX_BLOCKASSEMBLER_H #include "primitives/block.h" +#include "sapling/saplingscriptpubkeyman.h" +#include "stakeinput.h" #include "txmempool.h" -#include -#include -#include "boost/multi_index_container.hpp" #include "boost/multi_index/ordered_index.hpp" +#include "boost/multi_index_container.hpp" +#include "wallet/wallet.h" +#include +#include class CBlockIndex; class CChainParams; @@ -163,14 +166,14 @@ class BlockAssembler BlockAssembler(const CChainParams& chainparams, const bool defaultPrintPriority); /** Construct a new block template with coinbase to scriptPubKeyIn */ std::unique_ptr CreateNewBlock(const CScript& scriptPubKeyIn, - CWallet* pwallet = nullptr, - bool fProofOfStake = false, - std::vector* availableCoins = nullptr, - bool fNoMempoolTx = false, - bool fTestValidity = true, - CBlockIndex* prevBlock = nullptr, - bool stopPoSOnNewBlock = true, - bool fIncludeQfc = true); + CWallet* pwallet = nullptr, + bool fProofOfStake = false, + const std::vector>& availableCoins = {}, + bool fNoMempoolTx = false, + bool fTestValidity = true, + CBlockIndex* prevBlock = nullptr, + bool stopPoSOnNewBlock = true, + bool fIncludeQfc = true); private: // utility functions diff --git a/src/blocksignature.cpp b/src/blocksignature.cpp index a8f21904c6f04..a45328c7d7ddc 100644 --- a/src/blocksignature.cpp +++ b/src/blocksignature.cpp @@ -4,6 +4,7 @@ #include "blocksignature.h" +#include "rust/include/librustzcash.h" #include "script/standard.h" #include "zpiv/zpivmodule.h" @@ -32,9 +33,19 @@ bool SignBlockWithKey(CBlock& block, const CKey& key) return true; } -bool SignBlock(CBlock& block, const CKeyStore& keystore) +bool SignBlock(CBlock& block, const CKeyStore& keystore, Optional shieldStakeRandomness, Optional shieldStakePrivKey) { CKeyID keyID; + + if (block.IsProofOfShieldStake()) { + block.vchBlockSig.resize(64); + if (shieldStakeRandomness == boost::none || shieldStakePrivKey == boost::none) { + return error("%s: failed to find keys for shield stake", __func__); + } + if (!librustzcash_sign_block((*shieldStakePrivKey).begin(), (*shieldStakeRandomness).begin(), block.GetHash().begin(), block.vchBlockSig.data())) return error("%s: failed to sign the shield stake block", __func__); + return true; + } + if (!GetKeyIDFromUTXO(block.vtx[1]->vout[1], keyID)) { return error("%s: failed to find key for PoS", __func__); } @@ -50,7 +61,14 @@ bool CheckBlockSignature(const CBlock& block) { if (block.IsProofOfWork()) return block.vchBlockSig.empty(); + if (block.IsProofOfShieldStake()) { + uint256 rk = block.vtx[1]->sapData->vShieldedSpend[0].rk; + if (block.vchBlockSig.size() != 64) { + return error("%s: vchBlockSig has not the right length!", __func__); + } + return librustzcash_verify_block_signature(rk.begin(), block.GetHash().begin(), block.vchBlockSig.data()); + } if (block.vchBlockSig.empty()) return error("%s: vchBlockSig is empty!", __func__); diff --git a/src/blocksignature.h b/src/blocksignature.h index 84b59dbcf8328..51b202eeb783f 100644 --- a/src/blocksignature.h +++ b/src/blocksignature.h @@ -6,11 +6,13 @@ #define PIVX_BLOCKSIGNATURE_H #include "key.h" -#include "primitives/block.h" #include "keystore.h" +#include "optional.h" +#include "primitives/block.h" +#include "sapling/transaction_builder.h" bool SignBlockWithKey(CBlock& block, const CKey& key); -bool SignBlock(CBlock& block, const CKeyStore& keystore); +bool SignBlock(CBlock& block, const CKeyStore& keystore, Optional shieldStakeRandomness = boost::none, Optional shieldStakePrivKey = boost::none); bool CheckBlockSignature(const CBlock& block); #endif //PIVX_BLOCKSIGNATURE_H diff --git a/src/budget/budgetmanager.cpp b/src/budget/budgetmanager.cpp index 9cf51d3b9133b..6e7c95396bac3 100644 --- a/src/budget/budgetmanager.cpp +++ b/src/budget/budgetmanager.cpp @@ -526,6 +526,13 @@ bool CBudgetManager::FillBlockPayee(CMutableTransaction& txCoinbase, CMutableTra const bool fPayCoinstake = fProofOfStake && !Params().GetConsensus().NetworkUpgradeActive(nHeight, Consensus::UPGRADE_V6_0); + + // With current implementation shield staking must be activated NOT BEFORE THE V6.0 + // In other words budgets must be payed in coinbase, the opposite case would just give an invalid block + bool isShieldStake = Params().GetConsensus().NetworkUpgradeActive(nHeight, Consensus::UPGRADE_SHIELD_STAKING); + if (isShieldStake) + assert(!fPayCoinstake); + if (fProofOfStake) { if (fPayCoinstake) { unsigned int i = txCoinstake.vout.size(); diff --git a/src/chainparams.cpp b/src/chainparams.cpp index e07477288c4b8..1c0cf3f692c63 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -9,6 +9,7 @@ #include "chainparamsseeds.h" #include "consensus/merkle.h" +#include "consensus/params.h" #include "tinyformat.h" #include "utilstrencodings.h" @@ -295,6 +296,7 @@ class CMainParams : public CChainParams consensus.vUpgrades[Consensus::UPGRADE_V5_5].nActivationHeight = 3715200; consensus.vUpgrades[Consensus::UPGRADE_V6_0].nActivationHeight = Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT; + consensus.vUpgrades[Consensus::UPGRADE_SHIELD_STAKING].nActivationHeight = Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT; consensus.vUpgrades[Consensus::UPGRADE_ZC].hashActivationBlock = uint256S("0x5b2482eca24caf2a46bb22e0545db7b7037282733faa3a42ec20542509999a64"); @@ -453,6 +455,7 @@ class CTestNetParams : public CChainParams consensus.vUpgrades[Consensus::UPGRADE_V5_5].nActivationHeight = 925056; consensus.vUpgrades[Consensus::UPGRADE_V6_0].nActivationHeight = Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT; + consensus.vUpgrades[Consensus::UPGRADE_SHIELD_STAKING].nActivationHeight = Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT; /** * The message start string is designed to be unlikely to occur in normal data. @@ -601,8 +604,8 @@ class CRegTestParams : public CChainParams consensus.vUpgrades[Consensus::UPGRADE_V5_2].nActivationHeight = 300; consensus.vUpgrades[Consensus::UPGRADE_V5_3].nActivationHeight = 251; consensus.vUpgrades[Consensus::UPGRADE_V5_5].nActivationHeight = 576; - consensus.vUpgrades[Consensus::UPGRADE_V6_0].nActivationHeight = - Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT; + consensus.vUpgrades[Consensus::UPGRADE_V6_0].nActivationHeight = 600; + consensus.vUpgrades[Consensus::UPGRADE_SHIELD_STAKING].nActivationHeight = 601; /** * The message start string is designed to be unlikely to occur in normal data. diff --git a/src/consensus/params.h b/src/consensus/params.h index b8523b6b14ead..738ac488d84f0 100644 --- a/src/consensus/params.h +++ b/src/consensus/params.h @@ -38,6 +38,7 @@ enum UpgradeIndex : uint32_t { UPGRADE_V5_3, UPGRADE_V5_5, UPGRADE_V6_0, + UPGRADE_SHIELD_STAKING, UPGRADE_TESTDUMMY, // NOTE: Also add new upgrades to NetworkUpgradeInfo in upgrades.cpp MAX_NETWORK_UPGRADES diff --git a/src/consensus/tx_verify.cpp b/src/consensus/tx_verify.cpp index 027a0eb3ae849..fc9d48f058bc2 100644 --- a/src/consensus/tx_verify.cpp +++ b/src/consensus/tx_verify.cpp @@ -87,7 +87,7 @@ bool CheckTransaction(const CTransaction& tx, CValidationState& state, bool fCol // Check for negative or overflow output values const Consensus::Params& consensus = Params().GetConsensus(); for (const CTxOut& txout : tx.vout) { - if (txout.IsEmpty() && !tx.IsCoinBase() && !tx.IsCoinStake()) + if (txout.IsEmpty() && !tx.IsCoinBase() && !tx.IsCoinStake() && !tx.IsCoinShieldStake()) return state.DoS(100, false, REJECT_INVALID, "bad-txns-vout-empty"); if (txout.nValue < 0) return state.DoS(100, false, REJECT_INVALID, "bad-txns-vout-negative"); diff --git a/src/consensus/upgrades.cpp b/src/consensus/upgrades.cpp index e313d831c9478..917511c7e9016 100644 --- a/src/consensus/upgrades.cpp +++ b/src/consensus/upgrades.cpp @@ -13,66 +13,70 @@ * We are using it in the -nuparams startup arg and input it with spaces is just ugly. */ const struct NUInfo NetworkUpgradeInfo[Consensus::MAX_NETWORK_UPGRADES] = { - { - /*.strName =*/ "Base", - /*.strInfo =*/ "PIVX network", - }, - { - /*.strName =*/ "PoS", - /*.strInfo =*/ "Proof of Stake Consensus activation", - }, - { - /*.strName =*/ "PoS_v2", - /*.strInfo =*/ "New selection for stake modifier", - }, - { - /*.strName =*/ "Zerocoin", - /*.strInfo =*/ "ZeroCoin protocol activation - start block v4", - }, - { - /*.strName =*/ "Zerocoin_v2", - /*.strInfo =*/ "New zerocoin serials and zPOS start", - }, - { - /*.strName =*/ "BIP65", - /*.strInfo =*/ "CLTV (BIP65) activation - start block v5", - }, - { - /*.strName =*/ "Zerocoin_Public", - /*.strInfo =*/ "Activation of zerocoin public spends (spend v3)", - }, - { - /*.strName =*/ "PIVX_v3.4", - /*.strInfo =*/ "New 256-bit stake modifier - start block v6", - }, - { - /*.strName =*/ "PIVX_v4.0", - /*.strInfo =*/ "New message sigs - start block v7 - time protocol - zc spend v4", - }, - { - /*.strName =*/ "v5_shield", - /*.strInfo =*/ "Sapling Shield - start block v8 - start transaction v3", - }, - { - /*.strName =*/ "PIVX_v5.2", - /*.strInfo =*/ "New cold-staking rules", - }, - { - /*.strName =*/ "PIVX_v5.3", - /*.strInfo =*/ "New staking rules", - }, - { - /*.strName =*/ "PIVX_v5.5", - /*.strInfo =*/ "New rewards structure", - }, - { - /*.strName =*/ "v6_evo", - /*.strInfo =*/ "Deterministic Masternodes", - }, - { - /*.strName =*/ "Test_dummy", - /*.strInfo =*/ "Test dummy info", - }, + { + /*.strName =*/"Base", + /*.strInfo =*/"PIVX network", + }, + { + /*.strName =*/"PoS", + /*.strInfo =*/"Proof of Stake Consensus activation", + }, + { + /*.strName =*/"PoS_v2", + /*.strInfo =*/"New selection for stake modifier", + }, + { + /*.strName =*/"Zerocoin", + /*.strInfo =*/"ZeroCoin protocol activation - start block v4", + }, + { + /*.strName =*/"Zerocoin_v2", + /*.strInfo =*/"New zerocoin serials and zPOS start", + }, + { + /*.strName =*/"BIP65", + /*.strInfo =*/"CLTV (BIP65) activation - start block v5", + }, + { + /*.strName =*/"Zerocoin_Public", + /*.strInfo =*/"Activation of zerocoin public spends (spend v3)", + }, + { + /*.strName =*/"PIVX_v3.4", + /*.strInfo =*/"New 256-bit stake modifier - start block v6", + }, + { + /*.strName =*/"PIVX_v4.0", + /*.strInfo =*/"New message sigs - start block v7 - time protocol - zc spend v4", + }, + { + /*.strName =*/"v5_shield", + /*.strInfo =*/"Sapling Shield - start block v8 - start transaction v3", + }, + { + /*.strName =*/"PIVX_v5.2", + /*.strInfo =*/"New cold-staking rules", + }, + { + /*.strName =*/"PIVX_v5.3", + /*.strInfo =*/"New staking rules", + }, + { + /*.strName =*/"PIVX_v5.5", + /*.strInfo =*/"New rewards structure", + }, + { + /*.strName =*/"v6_evo", + /*.strInfo =*/"Deterministic Masternodes", + }, + { + /*.strName =*/"shield_staking", + /*.strInfo =*/"Shield Staking", + }, + { + /*.strName =*/"Test_dummy", + /*.strInfo =*/"Test dummy info", + }, }; UpgradeState NetworkUpgradeState( diff --git a/src/kernel.cpp b/src/kernel.cpp index c78896c387dd4..7d66fafb47137 100644 --- a/src/kernel.cpp +++ b/src/kernel.cpp @@ -7,15 +7,21 @@ #include "kernel.h" +#include "arith_uint256.h" +#include "chainparams.h" +#include "consensus/params.h" +#include "consensus/validation.h" #include "db.h" #include "legacy/stakemodifier.h" #include "policy/policy.h" +#include "sapling/sapling_validation.h" #include "script/interpreter.h" #include "stakeinput.h" #include "util/system.h" #include "utilmoneystr.h" #include "validation.h" #include "zpiv/zpos.h" +#include /** * CStakeKernel Constructor @@ -32,18 +38,28 @@ CStakeKernel::CStakeKernel(const CBlockIndex* const pindexPrev, CStakeInput* sta stakeValue(stakeInput->GetValue()) { // Set kernel stake modifier - if (!Params().GetConsensus().NetworkUpgradeActive(pindexPrev->nHeight + 1, Consensus::UPGRADE_V3_4)) { - uint64_t nStakeModifier = 0; - if (!GetOldStakeModifier(stakeInput, nStakeModifier)) - LogPrintf("%s : ERROR: Failed to get kernel stake modifier\n", __func__); - // Modifier v1 - stakeModifier << nStakeModifier; + if (!stakeInput->IsShieldPIV()) { + if (!Params().GetConsensus().NetworkUpgradeActive(pindexPrev->nHeight + 1, Consensus::UPGRADE_V3_4)) { + uint64_t nStakeModifier = 0; + if (!GetOldStakeModifier(stakeInput, nStakeModifier)) + LogPrintf("%s : ERROR: Failed to get kernel stake modifier\n", __func__); + // Modifier v1 + stakeModifier << nStakeModifier; + } else { + // Modifier v2 + stakeModifier << pindexPrev->GetStakeModifierV2(); + } + const CBlockIndex* pindexFrom = stakeInput->GetIndexFrom(); + nTimeBlockFrom = pindexFrom->nTime; } else { - // Modifier v2 - stakeModifier << pindexPrev->GetStakeModifierV2(); + if (!Params().GetConsensus().NetworkUpgradeActive(pindexPrev->nHeight + 1, Consensus::UPGRADE_SHIELD_STAKING)) { + LogPrintf("%s : ShieldStaking is not yet active!", __func__); + } else { + stakeModifier << pindexPrev->GetStakeModifierV2(); + } + // For ShieldStaking we have no knowledge on the note block, so we just set the time to 0 + nTimeBlockFrom = 0; } - const CBlockIndex* pindexFrom = stakeInput->GetIndexFrom(); - nTimeBlockFrom = pindexFrom->nTime; } // Return stake kernel hash @@ -55,7 +71,7 @@ uint256 CStakeKernel::GetHash() const } // Check that the kernel hash meets the target required -bool CStakeKernel::CheckKernelHash(bool fSkipLog) const +bool CStakeKernel::CheckKernelHash(bool fSkipLog) { // Get weighted target arith_uint256 bnTarget; @@ -65,7 +81,8 @@ bool CStakeKernel::CheckKernelHash(bool fSkipLog) const // Check PoS kernel hash const arith_uint256& hashProofOfStake = UintToArith256(GetHash()); const bool res = hashProofOfStake < bnTarget; - + suggestedValue = ComputeSuggestedValue(stakeValue, bnTarget, hashProofOfStake); + LogPrintf("%d\n", suggestedValue); if (!fSkipLog || res) { LogPrint(BCLog::STAKING, "%s : Proof Of Stake:" "\nstakeModifier=%s" @@ -82,6 +99,14 @@ bool CStakeKernel::CheckKernelHash(bool fSkipLog) const return res; } +CAmount CStakeKernel::ComputeSuggestedValue(CAmount stakevalue, const arith_uint256& bnTarget, const arith_uint256& hashProofOfStake) const +{ + arith_uint256 diff; + diff.SetCompact(nBits); + auto total = static_cast((((hashProofOfStake) / diff).Get64() + 1) * 100); + if (total > stakevalue) return stakevalue; + return total; +} /* * PoS Validation @@ -94,13 +119,21 @@ static bool LoadStakeInput(const CBlock& block, std::unique_ptr& st if (!block.IsProofOfStake()) return error("called on non PoS block"); - // Construct the stakeinput object - const CTxIn& txin = block.vtx[1]->vin[0]; - stake = txin.IsZerocoinSpend() ? - std::unique_ptr(CLegacyZPivStake::NewZPivStake(txin, nHeight)) : - std::unique_ptr(CPivStake::NewPivStake(txin, nHeight, block.nTime)); + if (block.IsProofOfShieldStake()) { + // Sapling data existence is guaranteed with isProofOfShieldStake call + const auto& saplingData = block.vtx[1]->sapData.get(); + stake = std::unique_ptr(CShieldStake::NewShieldStake(saplingData.vShieldedSpend.at(0), block.shieldStakeProof.amount, nHeight, block.nTime)); + return stake != nullptr; + } else { + // Construct the stakeinput object + const CTxIn& txin = block.vtx[1]->vin[0]; + + stake = txin.IsZerocoinSpend() ? + std::unique_ptr(CLegacyZPivStake::NewZPivStake(txin, nHeight)) : + std::unique_ptr(CPivStake::NewPivStake(txin, nHeight, block.nTime)); - return stake != nullptr; + return stake != nullptr; + } } /* @@ -112,7 +145,7 @@ static bool LoadStakeInput(const CBlock& block, std::unique_ptr& st * @param[in] nTimeTx new blocktime * @return bool true if stake kernel hash meets target protocol */ -bool Stake(const CBlockIndex* pindexPrev, CStakeInput* stakeInput, unsigned int nBits, int64_t& nTimeTx) +bool Stake(const CBlockIndex* pindexPrev, CStakeInput* stakeInput, unsigned int nBits, int64_t& nTimeTx, CAmount* suggestedValue) { if (!stakeInput) return false; @@ -123,9 +156,23 @@ bool Stake(const CBlockIndex* pindexPrev, CStakeInput* stakeInput, unsigned int // Verify Proof Of Stake CStakeKernel stakeKernel(pindexPrev, stakeInput, nBits, nTimeTx); - return stakeKernel.CheckKernelHash(true); + bool check = stakeKernel.CheckKernelHash(true); + if (suggestedValue) + *suggestedValue = stakeKernel.GetSuggestedValue(); + + return check; } +// This checks if the provided note value is valid +bool CheckShieldStakeValidity(const CBlock& block, std::string& strError, CShieldStake& stakeInput) +{ + CValidationState state; + if (!SaplingValidation::CheckShieldStake(block, state, Params())) { + strError = state.GetRejectReason(); + return false; + } + return true; +} /* * CheckProofOfStake Check if block has valid proof of stake @@ -153,9 +200,19 @@ bool CheckProofOfStake(const CBlock& block, std::string& strError, const CBlockI return false; } + if (stakeInput->IsShieldPIV()) { + // Check proof validity + // TODO: refactor, just for testing + auto& shieldStake = static_cast(*stakeInput); + + if (!CheckShieldStakeValidity(block, strError, shieldStake)) { + return false; + } + return true; + } + // zPoS disabled (ContextCheck) before blocks V7, and the tx input signature is in CoinSpend if (stakeInput->IsZPIV()) return true; - // Verify tx input signature CTxOut stakePrevout; if (!stakeInput->GetTxOutFrom(stakePrevout)) { @@ -175,7 +232,6 @@ bool CheckProofOfStake(const CBlock& block, std::string& strError, const CBlockI return true; } - /* * GetStakeKernelHash Return stake kernel of a block * diff --git a/src/kernel.h b/src/kernel.h index c096d70ac641c..8a13de0ced1d5 100644 --- a/src/kernel.h +++ b/src/kernel.h @@ -8,6 +8,7 @@ #ifndef PIVX_KERNEL_H #define PIVX_KERNEL_H +#include "arith_uint256.h" #include "stakeinput.h" class CStakeKernel { @@ -26,7 +27,11 @@ class CStakeKernel { uint256 GetHash() const; // Check that the kernel hash meets the target required - bool CheckKernelHash(bool fSkipLog = false) const; + bool CheckKernelHash(bool fSkipLog = false); + CAmount GetSuggestedValue() const + { + return suggestedValue; + } private: // kernel message hashed @@ -37,6 +42,8 @@ class CStakeKernel { // hash target unsigned int nBits{0}; // difficulty for the target CAmount stakeValue{0}; // target multiplier + CAmount suggestedValue{0}; + CAmount ComputeSuggestedValue(CAmount stakevalue, const arith_uint256& bnTarget, const arith_uint256& hashProofOfStake) const; }; /* PoS Validation */ @@ -50,7 +57,7 @@ class CStakeKernel { * @param[in] nTimeTx new blocktime * @return bool true if stake kernel hash meets target protocol */ -bool Stake(const CBlockIndex* pindexPrev, CStakeInput* stakeInput, unsigned int nBits, int64_t& nTimeTx); +bool Stake(const CBlockIndex* pindexPrev, CStakeInput* stakeInput, unsigned int nBits, int64_t& nTimeTx, CAmount* suggestedValue = nullptr); /* * CheckProofOfStake Check if block has valid proof of stake @@ -63,6 +70,17 @@ bool Stake(const CBlockIndex* pindexPrev, CStakeInput* stakeInput, unsigned int */ bool CheckProofOfStake(const CBlock& block, std::string& strError, const CBlockIndex* pindexPrev = nullptr); +/* + * CheckProofOfStake Check if block has valid proof of shield stake + * + * @param[in] block block with the proof being verified + * @param[out] strError string returning error message (if any, else empty) + * @param[in] pindexPrev index of the parent block + * (if nullptr, it will be searched in mapBlockIndex) + * @return bool true if the block has a valid proof of stake + */ +bool CheckProofOfShieldStake(const CBlock& block, std::string& strError); + /* * GetStakeKernelHash Return stake kernel of a block * diff --git a/src/masternode-payments.cpp b/src/masternode-payments.cpp index 9379a3a503a2a..c1bfaad317c6b 100644 --- a/src/masternode-payments.cpp +++ b/src/masternode-payments.cpp @@ -5,19 +5,20 @@ #include "masternode-payments.h" +#include "budget/budgetmanager.h" #include "chainparams.h" #include "evo/deterministicmns.h" #include "fs.h" -#include "budget/budgetmanager.h" #include "masternodeman.h" #include "netmessagemaker.h" -#include "tiertwo/netfulfilledman.h" #include "spork.h" #include "sync.h" +#include "tiertwo/netfulfilledman.h" #include "tiertwo/tiertwo_sync_state.h" #include "util/system.h" #include "utilmoneystr.h" #include "validation.h" +#include /** Object for who's going to get paid on which blocks */ @@ -369,6 +370,12 @@ void CMasternodePayments::FillBlockPayee(CMutableTransaction& txCoinbase, CMutab const int nHeight = pindexPrev->nHeight + 1; bool fPayCoinstake = fProofOfStake && !Params().GetConsensus().NetworkUpgradeActive(nHeight, Consensus::UPGRADE_V6_0); + // With current implementation shield staking must be activated NOT BEFORE THE V6.0 + // In other words Masternodes must be payed in coinbase, the opposite case would just give an invalid block + bool isShieldStake = Params().GetConsensus().NetworkUpgradeActive(nHeight, Consensus::UPGRADE_SHIELD_STAKING); + if (isShieldStake) + assert(!fPayCoinstake); + // if PoS block pays the coinbase, clear it first if (fProofOfStake && !fPayCoinstake) txCoinbase.vout.clear(); diff --git a/src/miner.cpp b/src/miner.cpp index ce42f821a6721..c1f372a7efa11 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -18,10 +18,12 @@ #include "policy/feerate.h" #include "primitives/block.h" #include "primitives/transaction.h" +#include "stakeinput.h" #include "timedata.h" #include "util/blockstatecatcher.h" #include "util/system.h" #include "utilmoneystr.h" +#include #ifdef ENABLE_WALLET #include "wallet/wallet.h" #endif @@ -96,18 +98,18 @@ bool ProcessBlockFound(const std::shared_ptr& pblock, CWallet& wal bool fGenerateBitcoins = false; bool fStakeableCoins = false; -void CheckForCoins(CWallet* pwallet, std::vector* availableCoins) +bool CheckForCoins(CWallet* pwallet, std::vector>* stakeableCoins) { if (!pwallet || !pwallet->pStakerStatus) - return; + return {}; // control the amount of times the client will check for mintable coins (every block) { WAIT_LOCK(g_best_block_mutex, lock); if (g_best_block == pwallet->pStakerStatus->GetLastHash()) - return; + return false; } - fStakeableCoins = pwallet->StakeableCoins(availableCoins); + return pwallet->StakeableCoins(stakeableCoins); } void BitcoinMiner(CWallet* pwallet, bool fProofOfStake) @@ -122,7 +124,7 @@ void BitcoinMiner(CWallet* pwallet, bool fProofOfStake) std::unique_ptr pReservekey = fProofOfStake ? nullptr : std::make_unique(pwallet); // Available UTXO set - std::vector availableCoins; + std::vector> availableStakes; unsigned int nExtraNonce = 0; while (fGenerateBitcoins || fProofOfStake) { @@ -139,13 +141,15 @@ void BitcoinMiner(CWallet* pwallet, bool fProofOfStake) } // update fStakeableCoins - CheckForCoins(pwallet, &availableCoins); + CheckForCoins(pwallet, &availableStakes); while ((g_connman && g_connman->GetNodeCount(CConnman::CONNECTIONS_ALL) == 0 && Params().MiningRequiresPeers()) || pwallet->IsLocked() || !fStakeableCoins || masternodeSync.NotCompleted()) { MilliSleep(5000); // Do another check here to ensure fStakeableCoins is updated - if (!fStakeableCoins) CheckForCoins(pwallet, &availableCoins); + if (!fStakeableCoins) { + CheckForCoins(pwallet, &availableStakes); + } } //search our map of hashed blocks, see if bestblock has been hashed yet @@ -168,8 +172,8 @@ void BitcoinMiner(CWallet* pwallet, bool fProofOfStake) unsigned int nTransactionsUpdatedLast = mempool.GetTransactionsUpdated(); std::unique_ptr pblocktemplate((fProofOfStake ? - BlockAssembler(Params(), DEFAULT_PRINTPRIORITY).CreateNewBlock(CScript(), pwallet, true, &availableCoins) : - CreateNewBlockWithKey(pReservekey, pwallet))); + BlockAssembler(Params(), DEFAULT_PRINTPRIORITY).CreateNewBlock(CScript(), pwallet, true, availableStakes) : + CreateNewBlockWithKey(pReservekey, pwallet))); if (!pblocktemplate) continue; std::shared_ptr pblock = std::make_shared(pblocktemplate->block); diff --git a/src/primitives/block.h b/src/primitives/block.h index ac8861d075c01..669fa9616cd4f 100644 --- a/src/primitives/block.h +++ b/src/primitives/block.h @@ -7,10 +7,12 @@ #ifndef BITCOIN_PRIMITIVES_BLOCK_H #define BITCOIN_PRIMITIVES_BLOCK_H -#include "primitives/transaction.h" #include "keystore.h" +#include "primitives/transaction.h" +#include "sapling/sapling_transaction.h" #include "serialize.h" #include "uint256.h" +#include /** Nodes collect new transactions into a block, hash them into a hash tree, * and scan through nonce values to make the block's hash satisfy proof-of-work @@ -23,7 +25,7 @@ class CBlockHeader { public: // header - static const int32_t CURRENT_VERSION=11; // since v5.2.99 + static const int32_t CURRENT_VERSION = 12; // since v6.0 int32_t nVersion; uint256 hashPrevBlock; uint256 hashMerkleRoot; @@ -75,6 +77,45 @@ class CBlockHeader } }; +class ShieldStakeProof +{ +public: + CAmount amount; + uint256 inputCv; + uint256 rk; + libzcash::GrothProof inputProof = {{0}}; + + uint256 outputCv; + uint256 epk; + uint256 cmu; + libzcash::GrothProof outputProof = {{0}}; + libzcash::GrothProof sig = {{0}}; + + void SetNull() + { + amount = 0; + inputCv.SetNull(); + rk.SetNull(); + inputProof = {{0}}; + outputCv.SetNull(); + epk.SetNull(); + cmu.SetNull(); + outputProof = {{0}}; + } + + SERIALIZE_METHODS(ShieldStakeProof, obj) + { + READWRITE(obj.amount); + READWRITE(obj.inputCv); + READWRITE(obj.rk); + READWRITE(obj.inputProof); + READWRITE(obj.epk); + READWRITE(obj.cmu); + READWRITE(obj.outputCv); + READWRITE(obj.outputProof); + READWRITE(obj.sig); + } +}; class CBlock : public CBlockHeader { @@ -85,6 +126,9 @@ class CBlock : public CBlockHeader // ppcoin: block signature - signed by one of the coin base txout[N]'s owner std::vector vchBlockSig; + // Shield Stake proof bytes only for version 12+ + ShieldStakeProof shieldStakeProof; + // memory only mutable bool fChecked{false}; @@ -103,8 +147,13 @@ class CBlock : public CBlockHeader { READWRITEAS(CBlockHeader, obj); READWRITE(obj.vtx); - if(obj.vtx.size() > 1 && obj.vtx[1]->IsCoinStake()) + if (obj.vtx.size() > 1 && (obj.vtx[1]->IsCoinStake() || obj.vtx[1]->IsCoinShieldStake())) READWRITE(obj.vchBlockSig); + + // Shield Staking Proof + if (obj.nVersion >= 12 && obj.IsProofOfShieldStake()) { + READWRITE(obj.shieldStakeProof); + } } void SetNull() @@ -113,6 +162,7 @@ class CBlock : public CBlockHeader vtx.clear(); fChecked = false; vchBlockSig.clear(); + shieldStakeProof.SetNull(); } CBlockHeader GetBlockHeader() const @@ -133,7 +183,12 @@ class CBlock : public CBlockHeader bool IsProofOfStake() const { - return (vtx.size() > 1 && vtx[1]->IsCoinStake()); + return (vtx.size() > 1 && vtx[1]->IsCoinStake()) || IsProofOfShieldStake(); + } + + bool IsProofOfShieldStake() const + { + return (vtx.size() > 1 && vtx[1]->IsCoinShieldStake()); } bool IsProofOfWork() const diff --git a/src/primitives/transaction.cpp b/src/primitives/transaction.cpp index e84af1dbf8dd2..4dff921b79656 100644 --- a/src/primitives/transaction.cpp +++ b/src/primitives/transaction.cpp @@ -110,7 +110,7 @@ size_t CTransaction::DynamicMemoryUsage() const /* For backward compatibility, the hash is initialized to 0. TODO: remove the need for this default constructor entirely. */ CTransaction::CTransaction() : vin(), vout(), nVersion(CTransaction::CURRENT_VERSION), nType(TxType::NORMAL), nLockTime(0), hash() {} -CTransaction::CTransaction(const CMutableTransaction &tx) : vin(tx.vin), vout(tx.vout), nVersion(tx.nVersion), nType(tx.nType), nLockTime(tx.nLockTime), sapData(tx.sapData), extraPayload(tx.extraPayload), hash(ComputeHash()) {} +CTransaction::CTransaction(const CMutableTransaction& tx) : vin(tx.vin), vout(tx.vout), nVersion(tx.nVersion), nType(tx.nType), nLockTime(tx.nLockTime), sapData(tx.sapData), extraPayload(tx.extraPayload), shieldStakeRandomness(tx.shieldStakeRandomness), shieldStakePrivKey(tx.shieldStakePrivKey), hash(ComputeHash()) {} CTransaction::CTransaction(CMutableTransaction &&tx) : vin(std::move(tx.vin)), vout(std::move(tx.vout)), nVersion(tx.nVersion), nType(tx.nType), nLockTime(tx.nLockTime), sapData(tx.sapData), extraPayload(tx.extraPayload), hash(ComputeHash()) {} bool CTransaction::HasZerocoinSpendInputs() const @@ -135,7 +135,8 @@ bool CTransaction::IsCoinStake() const { if (vin.empty()) return false; - + if (sapData && !sapData->vShieldedSpend.empty()) + return false; bool fAllowNull = vin[0].IsZerocoinSpend(); if (vin[0].prevout.IsNull() && !fAllowNull) return false; @@ -143,6 +144,19 @@ bool CTransaction::IsCoinStake() const return (vout.size() >= 2 && vout[0].IsEmpty()); } +// Vout[0] must be empty, no transparent input and non empty sapling input and output +bool CTransaction::IsCoinShieldStake() const +{ + if (!sapData) + return false; + if (sapData->vShieldedSpend.empty()) + return false; + if (!vin.empty()) + return false; + + return (vout.size() == 1 && vout[0].IsEmpty()); +} + bool CTransaction::HasP2CSOutputs() const { for(const CTxOut& txout : vout) { diff --git a/src/primitives/transaction.h b/src/primitives/transaction.h index eb073ed7cc971..409512f300e1c 100644 --- a/src/primitives/transaction.h +++ b/src/primitives/transaction.h @@ -274,6 +274,10 @@ class CTransaction Optional sapData{SaplingTxData()}; // Future: Don't initialize it by default Optional> extraPayload{nullopt}; // only available for special transaction types + // It's very convenient having shield stake signing keys here + Optional shieldStakeRandomness = boost::none; + Optional shieldStakePrivKey = boost::none; + /** Construct a CTransaction that qualifies as IsNull() */ CTransaction(); @@ -378,6 +382,7 @@ class CTransaction } bool IsCoinStake() const; + bool IsCoinShieldStake() const; bool HasP2CSOutputs() const; friend bool operator==(const CTransaction& a, const CTransaction& b) @@ -413,6 +418,10 @@ struct CMutableTransaction Optional sapData{SaplingTxData()}; // Future: Don't initialize it by default Optional> extraPayload{nullopt}; + // It's very convenient having shield stake signing keys here + Optional shieldStakeRandomness = boost::none; + Optional shieldStakePrivKey = boost::none; + CMutableTransaction(); CMutableTransaction(const CTransaction& tx); diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index c6767903d7d4c..045dc84ceba5b 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -168,6 +168,10 @@ UniValue blockToJSON(const CBlock& block, const CBlockIndex* tip, const CBlockIn result.pushKV("bits", strprintf("%08x", block.nBits)); result.pushKV("difficulty", GetDifficulty(blockindex)); result.pushKV("chainwork", blockindex->nChainWork.GetHex()); + if (block.IsProofOfShieldStake()) { + auto& p = block.shieldStakeProof; + result.pushKV("shieldproofamount", p.amount); + } if (blockindex->pprev) result.pushKV("previousblockhash", blockindex->pprev->GetBlockHash().GetHex()); diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index b582fe2d554d5..5421cdd3c2afb 100644 --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -16,6 +16,7 @@ #include "shutdown.h" #include "util/blockstatecatcher.h" #include "validationinterface.h" +#include #ifdef ENABLE_WALLET #include "wallet/rpcwallet.h" #include "wallet/db.h" @@ -41,14 +42,14 @@ UniValue generateBlocks(const Consensus::Params& consensus, while (nHeight < nHeightEnd && !ShutdownRequested()) { // Get available coins - std::vector availableCoins; - if (fPoS && !pwallet->StakeableCoins(&availableCoins)) { + std::vector> availableCoins; + if (fPoS && !(pwallet->StakeableCoins(&availableCoins))) { throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, "No available coins to stake"); } std::unique_ptr pblocktemplate(fPoS ? - BlockAssembler(Params(), DEFAULT_PRINTPRIORITY).CreateNewBlock(CScript(), pwallet, true, &availableCoins) : - CreateNewBlockWithScript(*coinbaseScript, pwallet)); + BlockAssembler(Params(), DEFAULT_PRINTPRIORITY).CreateNewBlock(CScript(), pwallet, true, availableCoins) : + CreateNewBlockWithScript(*coinbaseScript, pwallet)); if (!pblocktemplate.get()) break; std::shared_ptr pblock = std::make_shared(pblocktemplate->block); diff --git a/src/rust/include/librustzcash.h b/src/rust/include/librustzcash.h index a3ca5e8d32133..c87a564833b82 100644 --- a/src/rust/include/librustzcash.h +++ b/src/rust/include/librustzcash.h @@ -1,6 +1,7 @@ #ifndef LIBRUSTZCASH_INCLUDE_H_ #define LIBRUSTZCASH_INCLUDE_H_ +#include #include extern "C" { @@ -72,6 +73,19 @@ extern "C" { unsigned char *result ); + /// Verify the block signature for shield staking + bool librustzcash_verify_block_signature( + const unsigned char* rk, + const unsigned char* sighash, + const unsigned char* sign); + + /// Computes the signature for a shield staking block + bool librustzcash_sign_block( + const unsigned char* ask, + const unsigned char* ar, + const unsigned char* sighash, + unsigned char* result); + /// Computes the signature for each Spend description, given the key /// `ask`, the re-randomization `ar`, the 32-byte sighash `sighash`, /// and an output `result` buffer of 64-bytes for the signature. diff --git a/src/rust/src/rustzcash.rs b/src/rust/src/rustzcash.rs index 0b0c610d4296d..4b10ebae67205 100644 --- a/src/rust/src/rustzcash.rs +++ b/src/rust/src/rustzcash.rs @@ -588,9 +588,12 @@ pub extern "system" fn librustzcash_sapling_check_spend( }; // Deserialize the signature - let spend_auth_sig = match Signature::read(&(unsafe { &*spend_auth_sig })[..]) { - Ok(sig) => sig, - Err(_) => return false, + // Spend auth sig is not needed in shield stake proofs. + // See #2836 for details + let spend_auth_sig = if spend_auth_sig.is_null() { + None + } else { + Signature::read(&(unsafe { &*spend_auth_sig })[..]).ok() }; // Deserialize the proof @@ -599,12 +602,18 @@ pub extern "system" fn librustzcash_sapling_check_spend( Err(_) => return false, }; + let sighash_value = if sighash_value.is_null() { + [0u8; 32] + } else { + unsafe { *sighash_value } + }; + unsafe { &mut *ctx }.check_spend( &cv, anchor, unsafe { &*nullifier }, rk.clone(), - unsafe { &*sighash_value }, + &sighash_value, spend_auth_sig, zkproof.clone(), unsafe { SAPLING_SPEND_VK.as_ref() }.unwrap(), @@ -684,7 +693,15 @@ pub extern "system" fn librustzcash_sapling_final_check( Err(_) => return false, }; - unsafe { &*ctx }.final_check(value_balance, unsafe { &*sighash_value }, binding_sig) + // Sighash is not needed in Shield stake proof. + // See #2836 for details. + let sighash_value = if sighash_value.is_null() { + [0u8; 32] + } else { + unsafe { *sighash_value } + }; + + unsafe { &*ctx }.final_check(value_balance, &sighash_value, binding_sig) } #[no_mangle] @@ -941,6 +958,63 @@ pub extern "system" fn librustzcash_sapling_output_proof( true } +#[no_mangle] +pub extern "system" fn librustzcash_verify_block_signature( + rk: *const [c_uchar; 32], + sighash: *const [c_uchar; 32], + sign: *const [c_uchar; 64], +) -> bool { + // Deserialize rk + let rk = match redjubjub::PublicKey::read(&(unsafe { &*rk })[..]) { + Ok(p) => p, + Err(_) => return false, + }; + + // Deserialize the signature + let signature = match Signature::read(&(unsafe { &*sign })[..]) { + Ok(sig) => sig, + Err(_) => return false, + }; + rk.verify(unsafe { &*sighash }, &signature, SPENDING_KEY_GENERATOR) +} + +#[no_mangle] +pub extern "system" fn librustzcash_sign_block( + ask: *const [c_uchar; 32], + ar: *const [c_uchar; 32], + sighash: *const [c_uchar; 32], + result: *mut [c_uchar; 64], +) -> bool { + // The caller provides the re-randomization of `ak`. + let ar = { + let ar = Fr::from_repr(unsafe { *ar }); + if ar.is_some().into() { + ar.unwrap() + } else { + return false; + } + }; + + // The caller provides `ask`, the spend authorizing key. + let ask = match redjubjub::PrivateKey::read(&(unsafe { &*ask })[..]) { + Ok(p) => p, + Err(_) => return false, + }; + + // Initialize secure RNG + let mut rng = OsRng; + + // Do the signing + let rsk = ask.randomize(ar); + let sig = rsk.sign(unsafe { &*sighash }, &mut rng, SPENDING_KEY_GENERATOR); + + // Write out the signature + sig.write(&mut (unsafe { &mut *result })[..]) + .expect("result should be 64 bytes"); + + return true; +} + #[no_mangle] pub extern "system" fn librustzcash_sapling_spend_sig( ask: *const [c_uchar; 32], diff --git a/src/rust/src/tests/notes.rs b/src/rust/src/tests/notes.rs index 7cbf51bacf792..3ba0a40cb7867 100644 --- a/src/rust/src/tests/notes.rs +++ b/src/rust/src/tests/notes.rs @@ -1,5 +1,4 @@ -use crate::librustzcash_sapling_compute_cm; -use crate::librustzcash_sapling_compute_nf; +use crate::{librustzcash_sapling_compute_cm, librustzcash_sapling_compute_nf}; #[test] fn notes() { diff --git a/src/sapling/sapling_operation.cpp b/src/sapling/sapling_operation.cpp index 0c5b915b52d44..f4e6b8cbdb50c 100644 --- a/src/sapling/sapling_operation.cpp +++ b/src/sapling/sapling_operation.cpp @@ -590,3 +590,11 @@ OperationResult CheckTransactionSize(std::vector& recipients, } return OperationResult(true); } + +bool ComputeShieldStakeProof(CWallet& wallet, CBlock& block, CStakeableShieldNote& note, CAmount suggestedValue) +{ + assert(block.IsProofOfShieldStake()); + assert(note.note.value() >= suggestedValue); + block.shieldStakeProof.amount = suggestedValue; + return true; +} diff --git a/src/sapling/sapling_validation.cpp b/src/sapling/sapling_validation.cpp index 2216003a357cb..b53c25ab07c16 100644 --- a/src/sapling/sapling_validation.cpp +++ b/src/sapling/sapling_validation.cpp @@ -5,11 +5,12 @@ #include "sapling/sapling_validation.h" -#include "consensus/consensus.h" // for MAX_BLOCK_SIZE_CURRENT -#include "script/interpreter.h" // for SigHash +#include "consensus/consensus.h" // for MAX_BLOCK_SIZE_CURRENT +#include "consensus/upgrades.h" // for CurrentEpochBranchId() #include "consensus/validation.h" // for CValidationState -#include "util/system.h" // for error() -#include "consensus/upgrades.h" // for CurrentEpochBranchId() +#include "logging.h" +#include "script/interpreter.h" // for SigHash +#include "util/system.h" // for error() #include @@ -65,7 +66,7 @@ bool CheckTransactionWithoutProofVerification(const CTransaction& tx, CValidatio // NB: negative valueBalance "takes" money from the transparent value pool just as outputs do nValueOut += -tx.sapData->valueBalance; - if (!consensus.MoneyRange(nValueOut)) { + if (!tx.IsCoinShieldStake() && !consensus.MoneyRange(nValueOut)) { return state.DoS(100, error("%s: txout total out of range", __func__ ), REJECT_INVALID, "bad-txns-txouttotal-toolarge"); } @@ -225,5 +226,43 @@ bool ContextualCheckTransaction( return true; } +bool CheckShieldStake(const CBlock& block, CValidationState& state, const CChainParams& chainParams) +{ + // Check that the block is in Shield stake form, i.e. has 1 shield input, 1 shield output + if (!block.IsProofOfShieldStake()) { + return false; + } + LogPrintf("%d", block.shieldStakeProof.amount); + + const auto& saplingData = block.vtx[1].get()->sapData.get(); + auto ctx = librustzcash_sapling_verification_ctx_init(); + const auto& inputNote = saplingData.vShieldedSpend[0]; + const auto& p = block.shieldStakeProof; + const int DOS_LEVEL_BLOCK = 100; + + if (!librustzcash_sapling_check_spend(ctx, p.inputCv.begin(), inputNote.anchor.begin(), inputNote.nullifier.begin(), p.rk.begin(), p.inputProof.begin(), nullptr, nullptr)) { + librustzcash_sapling_verification_ctx_free(ctx); + return state.DoS( + DOS_LEVEL_BLOCK, + error("%s: Sapling spend description invalid", __func__), + REJECT_INVALID, "bad-txns-sapling-spend-description-invalid"); + } + + if (!librustzcash_sapling_check_output(ctx, p.outputCv.begin(), p.cmu.begin(), p.epk.begin(), p.outputProof.begin())) { + librustzcash_sapling_verification_ctx_free(ctx); + return state.DoS(100, error("%s: Sapling output description invalid", __func__), + REJECT_INVALID, "bad-txns-sapling-output-description-invalid"); + } + + if (!librustzcash_sapling_final_check(ctx, block.shieldStakeProof.amount, block.shieldStakeProof.sig.data(), nullptr)) { + librustzcash_sapling_verification_ctx_free(ctx); + return state.DoS( + 100, + error("%s: Sapling binding signature invalid", __func__), + REJECT_INVALID, "bad-txns-sapling-binding-signature-invalid"); + } + librustzcash_sapling_verification_ctx_free(ctx); + return true; +} } // End SaplingValidation namespace diff --git a/src/sapling/sapling_validation.h b/src/sapling/sapling_validation.h index d5df2fd5e0990..dd17f7219259d 100644 --- a/src/sapling/sapling_validation.h +++ b/src/sapling/sapling_validation.h @@ -25,6 +25,9 @@ bool ContextualCheckTransaction(const CTransaction &tx, CValidationState &state, const CChainParams &chainparams, int nHeight, bool isMined, bool sInitBlockDownload); +/** Check shield stake */ +bool CheckShieldStake(const CBlock& block, CValidationState& state, const CChainParams& chainParams); + }; // End SaplingValidation namespace #endif //PIVX_SAPLING_VALIDATION_H diff --git a/src/sapling/saplingscriptpubkeyman.cpp b/src/sapling/saplingscriptpubkeyman.cpp index d027a130e606d..cd0913f724584 100644 --- a/src/sapling/saplingscriptpubkeyman.cpp +++ b/src/sapling/saplingscriptpubkeyman.cpp @@ -7,7 +7,10 @@ #include "chain.h" // for CBlockIndex #include "primitives/transaction.h" +#include "sapling/sapling_transaction.h" #include "validation.h" // for ReadBlockFromDisk() +#include "wallet/wallet.h" +#include void SaplingScriptPubKeyMan::AddToSaplingSpends(const uint256& nullifier, const uint256& wtxid) { @@ -553,6 +556,57 @@ void SaplingScriptPubKeyMan::GetFilteredNotes( } } } +bool SaplingScriptPubKeyMan::GetStakeableNotes(std::vector* notes, int minDepth) +{ + bool foundNote = false; + LOCK(wallet->cs_wallet); + for (auto& p : wallet->mapWallet) { + const CWalletTx& wtx = p.second; + // Filter coinbase/coinstakes transactions that don't have Sapling outputs + if ((wtx.IsCoinBase() || wtx.IsCoinStake()) && wtx.mapSaplingNoteData.empty()) { + continue; + } + // Filter the transactions before checking for notes + const int depth = wtx.GetDepthInMainChain(); + if (!IsFinalTx(wtx.tx, wallet->GetLastBlockHeight() + 1, GetAdjustedTime()) || + depth < minDepth) { + continue; + } + for (const auto& it : wtx.mapSaplingNoteData) { + const SaplingOutPoint& op = it.first; + const SaplingNoteData& nd = it.second; + + // skip sent notes + if (!nd.IsMyNote()) continue; + + // recover plaintext and address + auto optNotePtAndAddress = wtx.DecryptSaplingNote(op); + assert(static_cast(optNotePtAndAddress)); + + const libzcash::SaplingIncomingViewingKey& ivk = *(nd.ivk); + const libzcash::SaplingNotePlaintext& notePt = optNotePtAndAddress->first; + const libzcash::SaplingPaymentAddress& pa = optNotePtAndAddress->second; + auto& note = notePt.note(ivk).get(); + + // skip notes which cannot be spent + if (!HaveSpendingKeyForPaymentAddress(pa)) { + continue; + } + + if (nd.nullifier && IsSaplingSpent(*nd.nullifier)) { + continue; + } + SaplingNoteEntry noteEntry = SaplingNoteEntry(op, pa, note, notePt.memo(), depth); + if (notes) { + notes->emplace_back(CStakeableShieldNote(noteEntry, *nd.nullifier)); + } else { + return true; + } + foundNote = true; + } + } + return foundNote; +} /* Return list of available notes and locked notes grouped by sapling address. */ std::map> SaplingScriptPubKeyMan::ListNotes() const @@ -1282,3 +1336,77 @@ uint256 SaplingScriptPubKeyMan::getCommonOVKFromSeed() const HDSeed seed{key.GetPrivKey()}; return ovkForShieldingFromTaddr(seed); } + +bool SaplingScriptPubKeyMan::ComputeShieldStakeProof(CBlock& block, CStakeableShieldNote& note, CAmount suggestedValue) +{ + assert(block.IsProofOfShieldStake()); + assert(note.note.value() >= suggestedValue); + + const auto& spendNote = block.vtx[1]->sapData->vShieldedSpend[0]; + auto* ctx = librustzcash_sapling_proving_ctx_init(); + libzcash::SaplingExtendedSpendingKey sk; + if (!wallet->GetSaplingExtendedSpendingKey(note.address, sk)) { + return false; + } + + uint256 alpha; + uint256 anchor; + uint256 dataToBeSigned; + std::vector> witnesses; + std::vector noteop; + noteop.emplace_back(note.op); + GetSaplingNoteWitnesses(noteop, witnesses, anchor); + + librustzcash_sapling_generate_r(alpha.begin()); + CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); + ss << witnesses[0]->path(); + std::vector witness(ss.begin(), ss.end()); + assert(anchor == spendNote.anchor); + if (!librustzcash_sapling_spend_proof(ctx, sk.expsk.full_viewing_key().ak.begin(), + sk.expsk.nsk.begin(), + note.note.d.data(), + note.note.r.begin(), + alpha.begin(), + note.note.value(), + anchor.begin(), + witness.data(), + block.shieldStakeProof.inputCv.begin(), + block.shieldStakeProof.rk.begin(), + block.shieldStakeProof.inputProof.begin())) { + librustzcash_sapling_proving_ctx_free(ctx); + return false; + } + uint256 dummyEsk; + + CAmount amount = note.note.value() - suggestedValue; + uint256 rcm; + librustzcash_sapling_generate_r(rcm.begin()); + libzcash::SaplingPaymentAddress paymentAddress(note.address); + const std::array emptyMemo = {{0xF6}}; + libzcash::SaplingNote dummyNote(paymentAddress.d, paymentAddress.pk_d, amount, rcm); + libzcash::SaplingNotePlaintext notePlaintext(dummyNote, emptyMemo); + auto res = notePlaintext.encrypt(dummyNote.pk_d); + if (!res) return false; + auto& encryptor = res->second; + + ss = CDataStream(SER_NETWORK, PROTOCOL_VERSION); + ss << paymentAddress; + std::vector addressBytes(ss.begin(), ss.end()); + + if (!librustzcash_sapling_output_proof(ctx, encryptor.get_esk().begin(), addressBytes.data(), rcm.begin(), amount, block.shieldStakeProof.outputCv.begin(), block.shieldStakeProof.outputProof.begin())) { + librustzcash_sapling_proving_ctx_free(ctx); + return false; + } + block.shieldStakeProof.cmu = *dummyNote.cmu(); + block.shieldStakeProof.epk = encryptor.get_epk(); + + if (!librustzcash_sapling_binding_sig(ctx, suggestedValue, dataToBeSigned.data(), block.shieldStakeProof.sig.begin())) { + librustzcash_sapling_proving_ctx_free(ctx); + return false; + } + + librustzcash_sapling_proving_ctx_free(ctx); + block.shieldStakeProof.amount = suggestedValue; + LogPrintf("%s : Shield Stake proof generated with value %d\n", __func__, suggestedValue); + return true; +} diff --git a/src/sapling/saplingscriptpubkeyman.h b/src/sapling/saplingscriptpubkeyman.h index 7385b72fbf3d9..3fd3ccdfddbf0 100644 --- a/src/sapling/saplingscriptpubkeyman.h +++ b/src/sapling/saplingscriptpubkeyman.h @@ -6,11 +6,11 @@ #define PIVX_SAPLINGSCRIPTPUBKEYMAN_H #include "consensus/consensus.h" +#include "sapling/incrementalmerkletree.h" #include "sapling/note.h" +#include "script/ismine.h" #include "wallet/hdchain.h" -#include "wallet/wallet.h" #include "wallet/walletdb.h" -#include "sapling/incrementalmerkletree.h" //! Size of witness cache // Should be large enough that we can expect not to reorg beyond our cache @@ -19,6 +19,7 @@ static const unsigned int WITNESS_CACHE_SIZE = DEFAULT_MAX_REORG_DEPTH + 1; class CBlock; class CBlockIndex; +class CStakeableShieldNote; /** Sapling note, its location in a transaction, and number of confirmations. */ struct SaplingNoteEntry @@ -37,6 +38,7 @@ struct SaplingNoteEntry int confirmations; }; + class SaplingNoteData { public: @@ -296,6 +298,9 @@ class SaplingScriptPubKeyMan { bool requireSpendingKey=true, bool ignoreLocked=true) const; + /* Return a list of notes that are stakable */ + bool GetStakeableNotes(std::vector* notes, int minDepth); + /* Return list of available notes grouped by sapling address. */ std::map> ListNotes() const; @@ -406,6 +411,8 @@ class SaplingScriptPubKeyMan { std::map mapSaplingNullifiersToNotes; + bool ComputeShieldStakeProof(CBlock& block, CStakeableShieldNote& note, CAmount suggestedValue); + private: /* Parent wallet */ CWallet* wallet{nullptr}; diff --git a/src/sapling/transaction_builder.cpp b/src/sapling/transaction_builder.cpp index 09ba15c20a638..702fddda04e48 100644 --- a/src/sapling/transaction_builder.cpp +++ b/src/sapling/transaction_builder.cpp @@ -5,10 +5,11 @@ #include "sapling/transaction_builder.h" -#include "script/sign.h" -#include "utilmoneystr.h" #include "consensus/upgrades.h" #include "policy/policy.h" +#include "script/script.h" +#include "script/sign.h" +#include "utilmoneystr.h" #include "validation.h" #include @@ -150,6 +151,11 @@ void TransactionBuilder::Clear() fee = -1; // Verified in Build(). Must be set before. } +void TransactionBuilder::AddStakeInput() +{ + mtx.vout.emplace_back(0, CScript()); +} + void TransactionBuilder::AddSaplingSpend( const libzcash::SaplingExpandedSpendingKey& expsk, const libzcash::SaplingNote& note, @@ -215,6 +221,11 @@ void TransactionBuilder::SetFee(CAmount _fee) this->fee = _fee; } +uint256 TransactionBuilder::GetShieldStakeRandomness() +{ + return spends[0].alpha; +} + void TransactionBuilder::SendChangeTo(const libzcash::SaplingPaymentAddress& changeAddr, const uint256& ovk) { saplingChangeAddr = std::make_pair(ovk, changeAddr); @@ -409,7 +420,8 @@ TransactionBuilderResult TransactionBuilder::Build(bool fDummySig) for (auto& tOut : mtx.vout) { change -= tOut.nValue; } - if (change < 0) { + // Change cannot be negative unless the tx is shield staking + if (change < 0 && !(mtx.vout.size() > 0 && mtx.vout[0].IsEmpty())) { return TransactionBuilderResult("Change cannot be negative"); } diff --git a/src/sapling/transaction_builder.h b/src/sapling/transaction_builder.h index 6de366cc4eb13..95264d51d42fa 100644 --- a/src/sapling/transaction_builder.h +++ b/src/sapling/transaction_builder.h @@ -104,6 +104,10 @@ class TransactionBuilder void SetFee(CAmount _fee); + // returns the randomness required to compute the blocksignature in shield stake + uint256 GetShieldStakeRandomness(); + + void AddStakeInput(); // Throws if the anchor does not match the anchor used by // previously-added Sapling spends. void AddSaplingSpend( diff --git a/src/stakeinput.cpp b/src/stakeinput.cpp index 44360dfb6c4e1..18b4789129b2b 100644 --- a/src/stakeinput.cpp +++ b/src/stakeinput.cpp @@ -4,7 +4,9 @@ #include "stakeinput.h" +#include "amount.h" #include "chain.h" +#include "streams.h" #include "txdb.h" #include "validation.h" @@ -87,6 +89,12 @@ CDataStream CPivStake::GetUniqueness() const return ss; } +CShieldStake* CShieldStake::NewShieldStake(const SpendDescription& spendDescription, CAmount noteAmount, int nHeight, uint32_t nTime) +{ + // TODO: add previous block and all of that stuff + return new CShieldStake(spendDescription.nullifier, noteAmount); +} + //The block that the UTXO was added to the chain const CBlockIndex* CPivStake::GetIndexFrom() const { @@ -95,3 +103,10 @@ const CBlockIndex* CPivStake::GetIndexFrom() const return pindexFrom; } +// A Unique identifier of a shield note +CDataStream CShieldStake::GetUniqueness() const +{ + CDataStream ss(SER_NETWORK, 0); + ss << this->nullifier; + return ss; +} diff --git a/src/stakeinput.h b/src/stakeinput.h index 45a56b83192b1..7a517bb8b7f3d 100644 --- a/src/stakeinput.h +++ b/src/stakeinput.h @@ -5,9 +5,15 @@ #ifndef PIVX_STAKEINPUT_H #define PIVX_STAKEINPUT_H +#include "amount.h" #include "chain.h" +#include "primitives/transaction.h" +#include "sapling/sapling_transaction.h" #include "streams.h" #include "uint256.h" +#include "validation.h" +#include +#include class CKeyStore; class CWallet; @@ -22,10 +28,14 @@ class CStakeInput CStakeInput(const CBlockIndex* _pindexFrom) : pindexFrom(_pindexFrom) {} virtual ~CStakeInput(){}; virtual const CBlockIndex* GetIndexFrom() const = 0; - virtual bool GetTxOutFrom(CTxOut& out) const = 0; virtual CAmount GetValue() const = 0; virtual bool IsZPIV() const = 0; + virtual bool IsShieldPIV() const = 0; virtual CDataStream GetUniqueness() const = 0; + virtual bool GetTxOutFrom(CTxOut& out) const = 0; + virtual CTxIn GetTxIn() const = 0; + // Return the basic info to understand if a CStakeInput has been spent or not + virtual std::pair GetSpendInfo() const = 0; }; @@ -45,9 +55,40 @@ class CPivStake : public CStakeInput bool GetTxOutFrom(CTxOut& out) const override; CAmount GetValue() const override; CDataStream GetUniqueness() const override; - CTxIn GetTxIn() const; + CTxIn GetTxIn() const override; bool IsZPIV() const override { return false; } + bool IsShieldPIV() const override { return false; }; + // The pair (tx hash, vout index) is enough to understand if the utxo has been spent + std::pair GetSpendInfo() const override { return {outpointFrom.hash, outpointFrom.n}; }; }; +class CShieldStake : public CStakeInput +{ +private: + // The nullifier is a unique identifier for the note + const uint256 nullifier; + // Despite what the name suggests, this is NOT the note value, but just a lower bound + const CAmount noteValue; + +public: + CShieldStake(uint256 _nullifier, CAmount _noteValue) : CStakeInput(nullptr), nullifier(_nullifier), noteValue(_noteValue) {} + static CShieldStake* NewShieldStake(const SpendDescription& spendDescription, CAmount noteValue, int nHeight, uint32_t nTime); + CAmount GetValue() const override { return noteValue; }; + CDataStream GetUniqueness() const override; + + const CBlockIndex* GetIndexFrom() const override { throw std::runtime_error("Cannot find the BlockIndex for a shield note"); }; + bool IsZPIV() const override { return false; } + bool IsShieldPIV() const override { return true; }; + bool GetTxOutFrom(CTxOut& out) const override + { + return false; + } + CTxIn GetTxIn() const override + { + throw new std::runtime_error("Cannot find Txin in a shield note"); + } + // The nullifier is enough to understand if the note has been spent + std::pair GetSpendInfo() const override { return {nullifier, 0}; }; +}; #endif //PIVX_STAKEINPUT_H diff --git a/src/test/test_pivx.cpp b/src/test/test_pivx.cpp index 4523b7565e568..7d9dd0fbf3354 100644 --- a/src/test/test_pivx.cpp +++ b/src/test/test_pivx.cpp @@ -201,15 +201,16 @@ CBlock TestChainSetup::CreateBlock(const std::vector& txns, CBlockIndex* customPrevBlock) { std::unique_ptr pblocktemplate = BlockAssembler( - Params(), DEFAULT_PRINTPRIORITY).CreateNewBlock(scriptPubKey, - nullptr, // wallet - false, // fProofOfStake - nullptr, // availableCoins - fNoMempoolTx, - fTestBlockValidity, - customPrevBlock, - true, - fIncludeQfc); + Params(), DEFAULT_PRINTPRIORITY) + .CreateNewBlock(scriptPubKey, + nullptr, // wallet + false, // fProofOfStake + {}, // availableCoins + fNoMempoolTx, + fTestBlockValidity, + customPrevBlock, + true, + fIncludeQfc); std::shared_ptr pblock = std::make_shared(pblocktemplate->block); // Add passed-in txns: diff --git a/src/test/validation_tests.cpp b/src/test/validation_tests.cpp index f71fde210681c..67949961d0eed 100644 --- a/src/test/validation_tests.cpp +++ b/src/test/validation_tests.cpp @@ -77,7 +77,7 @@ BOOST_FIXTURE_TEST_CASE(test_simple_shielded_invalid, TestingSetup) CMutableTransaction newTx(tx); CValidationState state; - // Create a coinstake transaction + // Create a mixed transaction with an empty vout and sapling data (so it isn't neither coinstake nor coinshieldstake) CTxIn vin; vin.prevout = COutPoint(UINT256_ZERO, 0); newTx.vin.emplace_back(vin); @@ -90,7 +90,7 @@ BOOST_FIXTURE_TEST_CASE(test_simple_shielded_invalid, TestingSetup) newTx.sapData->vShieldedSpend.emplace_back(); BOOST_CHECK(!CheckTransaction(newTx, state, false)); - BOOST_CHECK(state.GetRejectReason() == "bad-txns-invalid-sapling"); + BOOST_CHECK(state.GetRejectReason() == "bad-txns-vout-empty"); } } diff --git a/src/validation.cpp b/src/validation.cpp index bc8e163cb5ece..c268bd79d7e75 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -18,6 +18,7 @@ #include "checkqueue.h" #include "consensus/consensus.h" #include "consensus/merkle.h" +#include "consensus/params.h" #include "consensus/tx_verify.h" #include "consensus/validation.h" #include "consensus/zerocoin_verify.h" @@ -380,6 +381,10 @@ static bool AcceptToMemoryPoolWorker(CTxMemPool& pool, CValidationState &state, if (tx.IsCoinStake()) return state.DoS(100, false, REJECT_INVALID, "coinstake"); + // CoinShieldStake is also only valid in a block, not as a loose transaction + if (tx.IsCoinShieldStake()) + return state.DoS(100, false, REJECT_INVALID, "coinshieldstake"); + // LLMQ final commitment too, not valid as a loose transaction if (tx.IsQuorumCommitmentTx()) return state.DoS(100, false, REJECT_INVALID, "llmqcomm"); @@ -1077,7 +1082,7 @@ bool CheckTxInputs(const CTransaction& tx, CValidationState& state, const CCoins // Sapling nValueIn += tx.GetShieldedValueIn(); - if (!tx.IsCoinStake()) { + if (!tx.IsCoinStake() && !tx.IsCoinShieldStake()) { if (nValueIn < tx.GetValueOut()) return state.DoS(100, false, REJECT_INVALID, "bad-txns-in-belowout", false, strprintf("value in (%s) < value out (%s)", FormatMoney(nValueIn), FormatMoney(tx.GetValueOut()))); @@ -1446,7 +1451,7 @@ static bool ConnectBlock(const CBlock& block, CValidationState& state, CBlockInd const bool isPoSActive = consensus.NetworkUpgradeActive(pindex->nHeight, Consensus::UPGRADE_POS); const bool isV5UpgradeEnforced = consensus.NetworkUpgradeActive(pindex->nHeight, Consensus::UPGRADE_V5_0); const bool isV6UpgradeEnforced = consensus.NetworkUpgradeActive(pindex->nHeight, Consensus::UPGRADE_V6_0); - + const bool isShieldStakeActive = consensus.NetworkUpgradeActive(pindex->nHeight, Consensus::UPGRADE_SHIELD_STAKING); // Coinbase output should be empty if proof-of-stake block (before v6 enforcement) if (!isV6UpgradeEnforced && isPoSBlock && (block.vtx[0]->vout.size() != 1 || !block.vtx[0]->vout[0].IsEmpty())) return state.DoS(100, false, REJECT_INVALID, "bad-cb-pos", false, "coinbase output not empty for proof-of-stake block"); @@ -1476,6 +1481,11 @@ static bool ConnectBlock(const CBlock& block, CValidationState& state, CBlockInd return state.DoS(100, error("ConnectBlock() : PoW period ended"), REJECT_INVALID, "PoW-ended"); + // You cannot shield stake if network upgrade is not active yet + if (!isShieldStakeActive && block.IsProofOfShieldStake()) { + return state.DoS(100, false, REJECT_INVALID, "bad-scs-not-active", false, + "shield staking is not active yet"); + } // Sapling // Reject a block that results in a negative shielded value pool balance. // Description under ZIP209 turnstile violation. @@ -1567,7 +1577,7 @@ static bool ConnectBlock(const CBlock& block, CValidationState& state, CBlockInd CAmount txValueOut = tx.GetValueOut(); if (!tx.IsCoinBase()) { CAmount txValueIn = view.GetValueIn(tx); - if (!tx.IsCoinStake()) + if (!tx.IsCoinStake() && !tx.IsCoinShieldStake()) nFees += txValueIn - txValueOut; nValueIn += txValueIn; @@ -2471,7 +2481,13 @@ static CBlockIndex* AddToBlockIndex(const CBlock& block) EXCLUSIVE_LOCKS_REQUIRE } else { // compute and set new V2 stake modifier (hash of prevout and prevModifier) - pindexNew->SetNewStakeModifier(block.vtx[1]->vin[0].prevout.hash); + if (block.IsProofOfStake()) { + if (block.IsProofOfShieldStake()) { + pindexNew->SetNewStakeModifier(block.vtx[1]->sapData->vShieldedSpend[0].nullifier); + } else { + pindexNew->SetNewStakeModifier(block.vtx[1]->vin[0].prevout.hash); + } + } } } pindexNew->nTimeMax = (pindexNew->pprev ? std::max(pindexNew->pprev->nTimeMax, pindexNew->nTime) : pindexNew->nTime); @@ -2739,10 +2755,10 @@ bool CheckBlock(const CBlock& block, CValidationState& state, bool fCheckPOW, bo if (IsPoS) { // Second transaction must be coinstake, the rest must not be - if (block.vtx.empty() || !block.vtx[1]->IsCoinStake()) + if (block.vtx.empty() || !(block.vtx[1]->IsCoinStake() || block.vtx[1]->IsCoinShieldStake())) return state.DoS(100, false, REJECT_INVALID, "bad-cs-missing", false, "second tx is not coinstake"); for (unsigned int i = 2; i < block.vtx.size(); i++) - if (block.vtx[i]->IsCoinStake()) + if (block.vtx[i]->IsCoinStake() || block.vtx[i]->IsCoinShieldStake()) return state.DoS(100, false, REJECT_INVALID, "bad-cs-multiple", false, "more than one coinstake"); } @@ -2770,7 +2786,7 @@ bool CheckBlock(const CBlock& block, CValidationState& state, bool fCheckPOW, bo // that this block is invalid, so don't issue an outright ban. if (nHeight != 0 && !IsInitialBlockDownload()) { // Last output of Cold-Stake is not abused - if (IsPoS && !CheckColdStakeFreeOutput(*(block.vtx[1]), nHeight)) { + if ((block.IsProofOfStake() && !block.IsProofOfShieldStake()) && !CheckColdStakeFreeOutput(*(block.vtx[1]), nHeight)) { mapRejectedBlocks.emplace(block.GetHash(), GetTime()); return state.DoS(0, false, REJECT_INVALID, "bad-p2cs-outs", false, "invalid cold-stake output"); } @@ -2935,8 +2951,8 @@ bool ContextualCheckBlockHeader(const CBlockHeader& block, CValidationState& sta (block.nVersion < 5 && consensus.NetworkUpgradeActive(nHeight, Consensus::UPGRADE_BIP65)) || (block.nVersion < 6 && consensus.NetworkUpgradeActive(nHeight, Consensus::UPGRADE_V3_4)) || (block.nVersion < 7 && consensus.NetworkUpgradeActive(nHeight, Consensus::UPGRADE_V4_0)) || - (block.nVersion < 8 && consensus.NetworkUpgradeActive(nHeight, Consensus::UPGRADE_V5_0))) - { + (block.nVersion < 8 && consensus.NetworkUpgradeActive(nHeight, Consensus::UPGRADE_V5_0)) || + (block.nVersion < 12 && consensus.NetworkUpgradeActive(nHeight, Consensus::UPGRADE_SHIELD_STAKING))) { std::string stringErr = strprintf("rejected block version %d at height %d", block.nVersion, nHeight); return state.Invalid(false, REJECT_OBSOLETE, "bad-version", stringErr); } @@ -2985,6 +3001,27 @@ bool ContextualCheckBlock(const CBlock& block, CValidationState& state, CBlockIn } } } + if (block.IsProofOfShieldStake()) { + CTransactionRef csTx = block.vtx[1]; + // No transparent inputs + if (!csTx->vin.empty()) { + return state.DoS(100, false, REJECT_INVALID, "bad-scs-multi-inputs", false, + "invalid shield stake with transparent input"); + } + + // No more than one shield spend + if (csTx->sapData->vShieldedSpend.size() > 1) { + return state.DoS(100, false, REJECT_INVALID, "bad-scs-multi-inputs", false, + "invalid multi-inputs shield stake"); + } + + // Prevent multi-empty-outputs + for (size_t i = 1; i < csTx->vout.size(); i++) { + if (csTx->vout[i].IsEmpty()) { + return state.DoS(100, false, REJECT_INVALID, "bad-txns-vout-empty"); + } + } + } return true; } @@ -3134,7 +3171,8 @@ static bool CheckInBlockDoubleSpends(const CBlock& block, int nHeight, CValidati if (inblock_txes.find(it->hash) != inblock_txes.end()) { // the input spent was created as output of another in-block tx // this is not allowed for the coinstake input - if (*it == block.vtx[1]->vin[0].prevout) { + // this check is not needed in shield staking + if (!block.IsProofOfShieldStake() && *it == block.vtx[1]->vin[0].prevout) { return state.DoS(100, error("%s: coinstake input created in the same block", __func__)); } it = spent_outpoints.erase(it); @@ -3269,6 +3307,7 @@ static bool AcceptBlock(const CBlock& block, CValidationState& state, CBlockInde return state.DoS(100, false, REJECT_INVALID); bool isPoS = block.IsProofOfStake(); + bool isShieldPos = block.IsProofOfShieldStake(); if (isPoS) { std::string strError; if (!CheckProofOfStake(block, strError, pindexPrev)) @@ -3365,17 +3404,17 @@ static bool AcceptBlock(const CBlock& block, CValidationState& state, CBlockInde } } - // ZPOS contextual checks - const CTransaction& coinstake = *block.vtx[1]; - const CTxIn& coinstake_in = coinstake.vin[0]; - if (coinstake_in.IsZerocoinSpend()) { - libzerocoin::CoinSpend spend = ZPIVModule::TxInToZerocoinSpend(coinstake_in); - if (!ContextualCheckZerocoinSpend(coinstake, &spend, pindex->nHeight)) { - return state.DoS(100,error("%s: main chain ContextualCheckZerocoinSpend failed for tx %s", __func__, - coinstake.GetHash().GetHex()), REJECT_INVALID, "bad-txns-invalid-zpiv"); + // ZPOS contextual checks, not needed for shield stake + if (!isShieldPos) { + const CTransaction& coinstake = *block.vtx[1]; + const CTxIn& coinstake_in = coinstake.vin[0]; + if (coinstake_in.IsZerocoinSpend()) { + libzerocoin::CoinSpend spend = ZPIVModule::TxInToZerocoinSpend(coinstake_in); + if (!ContextualCheckZerocoinSpend(coinstake, &spend, pindex->nHeight)) { + return state.DoS(100, error("%s: main chain ContextualCheckZerocoinSpend failed for tx %s", __func__, coinstake.GetHash().GetHex()), REJECT_INVALID, "bad-txns-invalid-zpiv"); + } } } - } // Write block to history file diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 5668cfd869b0b..af4e8da60a605 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -4385,8 +4385,10 @@ UniValue getstakingstatus(const JSONRPCRequest& request) " \"haveconnections\": true|false, (boolean) whether network connections are present\n" " \"mnsync\": true|false, (boolean) whether the required masternode/spork data is synced\n" " \"walletunlocked\": true|false, (boolean) whether the wallet is unlocked\n" - " \"stakeablecoins\": n (numeric) number of stakeable UTXOs\n" - " \"stakingbalance\": d (numeric) PIV value of the stakeable coins (minus reserve balance, if any)\n" + " \"transparent_stakeable_coins\": n (numeric) number of transparent stakeable UTXOs\n" + " \"shield_stakeables_notes\": n (numeric) number of shield stakeable notes\n" + " \"transparent_staking_balance\": d (numeric) PIV value of the stakeable coins (minus reserve balance, if any)\n" + " \"shield_staking_balance\": d (numeric) shield PIV value of the stakeable coins (minus reserve balance, if any)\n" " \"stakesplitthreshold\": d (numeric) value of the current threshold for stake split\n" " \"lastattempt_age\": n (numeric) seconds since last stake attempt\n" " \"lastattempt_depth\": n (numeric) depth of the block on top of which the last stake attempt was made\n" @@ -4412,9 +4414,13 @@ UniValue getstakingstatus(const JSONRPCRequest& request) obj.pushKV("mnsync", !masternodeSync.NotCompleted()); obj.pushKV("walletunlocked", !pwallet->IsLocked()); std::vector vCoins; - pwallet->StakeableCoins(&vCoins); - obj.pushKV("stakeablecoins", (int)vCoins.size()); - obj.pushKV("stakingbalance", ValueFromAmount(pwallet->GetStakingBalance(fColdStaking))); + std::vector vShieldCoins; + pwallet->StakeableUTXOs(&vCoins); + pwallet->StakeableNotes(&vShieldCoins); + obj.pushKV("transparent_stakeable_coins", (int)vCoins.size()); + obj.pushKV("shield_stakeables_notes", (int)vShieldCoins.size()); + obj.pushKV("transparent_staking_balance", ValueFromAmount(pwallet->GetStakingBalance(fColdStaking))); + obj.pushKV("shield_staking_balance", ValueFromAmount(pwallet->GetShieldStakingBalance())); obj.pushKV("stakesplitthreshold", ValueFromAmount(pwallet->nStakeSplitThreshold)); CStakerStatus* ss = pwallet->pStakerStatus; if (ss) { diff --git a/src/wallet/test/pos_validations_tests.cpp b/src/wallet/test/pos_validations_tests.cpp index e07d89d1c5882..9ebf58469ba1c 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) @@ -43,21 +54,26 @@ BOOST_FIXTURE_TEST_CASE(coinstake_tests, TestPoSChainSetup) SyncWithValidationInterfaceQueue(); // Let's create the block - std::vector availableCoins; - BOOST_CHECK(pwalletMain->StakeableCoins(&availableCoins)); + std::vector availableUTXOs; + BOOST_CHECK(pwalletMain->StakeableUTXOs(&availableUTXOs)); + std::vector> availableCoins; + for (auto& utxo : availableUTXOs) { + availableCoins.push_back(std::make_unique(utxo)); + } std::unique_ptr pblocktemplate = BlockAssembler( - Params(), false).CreateNewBlock(CScript(), - pwalletMain.get(), - true, - &availableCoins, - true); + Params(), false) + .CreateNewBlock(CScript(), + pwalletMain.get(), + true, + availableCoins, + true); std::shared_ptr pblock = std::make_shared(pblocktemplate->block); BOOST_CHECK(pblock->IsProofOfStake()); // Add a second input to a coinstake CMutableTransaction mtx(*pblock->vtx[1]); - const CStakeableOutput& in2 = availableCoins.back(); - availableCoins.pop_back(); + const CStakeableOutput& in2 = availableUTXOs.back(); + availableUTXOs.pop_back(); CTxIn vin2(in2.tx->GetHash(), in2.i); mtx.vin.emplace_back(vin2); @@ -138,43 +154,57 @@ 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 availableCoins; - BOOST_CHECK(pwalletMain->StakeableCoins(&availableCoins)); - - // 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 = availableCoins.begin(); it != availableCoins.end() ;) { - if (it->nDepth <= 120 || IsSpentOnFork(*it, forkchain)) { - it = availableCoins.erase(it); - } else { - it++; + 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++; + } } - } + 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::unique_ptr pblocktemplate = BlockAssembler( - Params(), false).CreateNewBlock(CScript(), - pwalletMain, - true, - &availableCoins, - true, - false, - customPrevBlock, - false); + Params(), false) + .CreateNewBlock(CScript(), + pwalletMain, + true, + availableCoins, + 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; @@ -438,4 +468,292 @@ 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 (we are spending both to be sure). + SaplingOperation operation = CreateOperationAndBuildTx(pwalletMain, 11000 * COIN, false); + // Sanity check on the notes that was spent + auto vecShieldSpend = operation.getFinalTx().sapData->vShieldedSpend; + BOOST_CHECK(vecShieldSpend[0].nullifier == shieldStakeNullifier || vecShieldSpend[1].nullifier == shieldStakeNullifier); + BOOST_CHECK(operation.getFinalTx().sapData->vShieldedSpend.size() == 2); + + // 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() diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 5774b168454ff..d8d7a53207a80 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -5,7 +5,21 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. +#include "consensus/params.h" #include "optional.h" +#include "primitives/transaction.h" +#include "sapling/address.h" +#include "sapling/incrementalmerkletree.h" +#include "sapling/note.h" +#include "sapling/sapling_transaction.h" +#include "sapling/sapling_util.h" +#include "sapling/saplingscriptpubkeyman.h" +#include "sapling/zip32.h" +#include "serialize.h" +#include "stakeinput.h" +#include "version.h" +#include +#include #if defined(HAVE_CONFIG_H) #include "config/pivx-config.h" #endif @@ -18,8 +32,9 @@ #include "guiinterfaceutil.h" #include "policy/policy.h" #include "sapling/key_io_sapling.h" -#include "script/sign.h" +#include "sapling/transaction_builder.h" #include "scheduler.h" +#include "script/sign.h" #include "shutdown.h" #include "spork.h" #include "util/validation.h" @@ -707,6 +722,11 @@ bool CWallet::IsSaplingSpent(const SaplingOutPoint& op) const return m_sspk_man->IsSaplingSpent(op); } +bool CWallet::IsSaplingSpent(const uint256& nullifier) const +{ + return m_sspk_man->IsSaplingSpent(nullifier); +} + /** * Outpoint is spent if any non-conflicted transaction * spends it: @@ -2179,6 +2199,17 @@ CAmount CWallet::GetStakingBalance(const bool fIncludeColdStaking) const })); } +CAmount CWallet::GetShieldStakingBalance() +{ + std::vector vShieldCoins; + CAmount total = 0; + this->StakeableNotes(&vShieldCoins); + for (auto it = vShieldCoins.begin(); it < vShieldCoins.end(); it++) { + total += it->note.value(); + } + return total; +} + CAmount CWallet::GetDelegatedBalance() const { return loopTxsBalance([](const uint256& id, const CWalletTx& pcoin, CAmount& nTotal) { @@ -2678,13 +2709,13 @@ static void ApproximateBestSubset(const std::vector* pCoins) +bool CWallet::StakeableUTXOs(std::vector* stakeableCoins) const { const bool fIncludeColdStaking = !sporkManager.IsSporkActive(SPORK_19_COLDSTAKING_MAINTENANCE) && gArgs.GetBoolArg("-coldstaking", DEFAULT_COLDSTAKING); - - if (pCoins) pCoins->clear(); - + bool fFoundUTXO = false; + // Add notes to stakablecoins + // Add UTXOs LOCK2(cs_main, cs_wallet); for (const auto& it : mapWallet) { const uint256& wtxid = it.first; @@ -2714,13 +2745,44 @@ bool CWallet::StakeableCoins(std::vector* pCoins) if (!res.available || !res.spendable) continue; - // found valid coin - if (!pCoins) return true; if (!pindex) pindex = mapBlockIndex.at(pcoin->m_confirm.hashBlock); - pCoins->emplace_back(pcoin, (int) index, nDepth, pindex); + if (!stakeableCoins) { + return true; + } + stakeableCoins->emplace_back(pcoin, (int)index, nDepth, pindex); + fFoundUTXO = true; + } + } + return fFoundUTXO; +} + +bool CWallet::StakeableNotes(std::vector* shieldNotes) const +{ + LOCK(cs_wallet); + if (Params().GetConsensus().NetworkUpgradeActive(this->GetLastBlockHeight(), Consensus::UPGRADE_SHIELD_STAKING)) { + return m_sspk_man->GetStakeableNotes(shieldNotes, Params().GetConsensus().nStakeMinDepth); + } + return false; +} + +bool CWallet::StakeableCoins(std::vector>* stakeableCoins) const +{ + assert(stakeableCoins || stakeableCoins->empty()); + // Add shield notes first + std::vector notes; + bool hasNotes = StakeableNotes(¬es); + // Add UTXOs + std::vector utxos; + bool hasUTXOs = StakeableUTXOs(&utxos); + if (stakeableCoins) { + for (const auto& note : notes) { + stakeableCoins->push_back(std::make_unique(note)); + } + for (const auto& utxo : utxos) { + stakeableCoins->push_back(std::make_unique(utxo)); } } - return (pCoins && !pCoins->empty()); + return hasNotes || hasUTXOs; } bool CWallet::SelectCoinsMinConf(const CAmount& nTargetValue, int nConfMine, int nConfTheirs, uint64_t nMaxAncestors, std::vector vCoins, std::set >& setCoinsRet, CAmount& nValueRet) const @@ -3294,7 +3356,7 @@ int CWallet::GetLastBlockHeightLockWallet() const return WITH_LOCK(cs_wallet, return m_last_block_processed_height;); } -bool CWallet::CreateCoinstakeOuts(const CPivStake& stakeInput, std::vector& vout, CAmount nTotal) const +bool CWallet::CreateCoinstakeOuts(const CStakeInput& stakeInput, std::vector& vout, CAmount nTotal) const { std::vector vSolutions; txnouttype whichType; @@ -3336,18 +3398,8 @@ bool CWallet::CreateCoinstakeOuts(const CPivStake& stakeInput, std::vector* availableCoins, - bool stopOnNewBlock) const +CStakeableInterface* CWallet::CreateCoinStake(const CBlockIndex& indexPrev, unsigned int nBits, CMutableTransaction& txNew, int64_t& nTxNewTime, const std::vector>& stakeOutputs, bool stopOnNewBlock) { - // shuffle coins - if (availableCoins && Params().IsRegTestNet()) { - Shuffle(availableCoins->begin(), availableCoins->end(), FastRandomContext()); - } // Mark coin stake transaction txNew.vin.clear(); @@ -3355,93 +3407,120 @@ bool CWallet::CreateCoinStake( txNew.vout.emplace_back(0, CScript()); // update staker status (hash) - pStakerStatus->SetLastTip(pindexPrev); - pStakerStatus->SetLastCoins((int) availableCoins->size()); + pStakerStatus->SetLastTip(&indexPrev); + pStakerStatus->SetLastCoins((int)stakeOutputs.size()); - // Kernel Search - CAmount nCredit; - CAmount nMasternodePayment; - CScript scriptPubKeyKernel; - bool fKernelFound = false; int nAttempts = 0; - for (auto it = availableCoins->begin(); it != availableCoins->end();) { - COutPoint outPoint = COutPoint(it->tx->GetHash(), it->i); - CPivStake stakeInput(it->tx->tx->vout[it->i], - outPoint, - it->pindex); - + // Kernel Search + for (const auto& stakeOutput : stakeOutputs) { + const auto& stakeInput = stakeOutput->ToInput(); // New block came in, move on - if (stopOnNewBlock && GetLastBlockHeightLockWallet() != pindexPrev->nHeight) return false; + if (stopOnNewBlock && GetLastBlockHeightLockWallet() != indexPrev.nHeight) return nullptr; // Make sure the wallet is unlocked and shutdown hasn't been requested - if (IsLocked() || ShutdownRequested()) return false; + if (IsLocked() || ShutdownRequested()) return nullptr; // Make sure the stake input hasn't been spent since last check - if (WITH_LOCK(cs_wallet, return IsSpent(outPoint))) { - // remove it from the available coins - it = availableCoins->erase(it); - continue; + if (stakeInput->IsShieldPIV()) { + if (WITH_LOCK(cs_wallet, return IsSaplingSpent(stakeInput->GetSpendInfo().first))) { + continue; + } + } else if (!stakeInput->IsZPIV()) { + if (WITH_LOCK(cs_wallet, return IsSpent(COutPoint(stakeInput->GetSpendInfo().first, stakeInput->GetSpendInfo().second)))) { + continue; + } } + ++nAttempts; - nCredit = 0; - - nAttempts++; - fKernelFound = Stake(pindexPrev, &stakeInput, nBits, nTxNewTime); + CAmount suggestedValue; + bool fKernelFound = Stake(&indexPrev, &*stakeInput, nBits, nTxNewTime, &suggestedValue); + // Refactor maybe + if (stakeInput->IsShieldPIV()) { + static_cast(stakeOutput.get())->suggestedValue = suggestedValue; + } - // update staker status (time, attempts) + // update staker status (time, attemps) pStakerStatus->SetLastTime(nTxNewTime); pStakerStatus->SetLastTries(nAttempts); if (!fKernelFound) { - it++; continue; } // Found a kernel LogPrintf("CreateCoinStake : kernel found\n"); - nCredit += stakeInput.GetValue(); // Add block reward to the credit - nCredit += GetBlockValue(pindexPrev->nHeight + 1); - nMasternodePayment = GetMasternodePayment(pindexPrev->nHeight + 1); - - // Create the output transaction(s) - std::vector vout; - if (!CreateCoinstakeOuts(stakeInput, vout, nCredit - nMasternodePayment)) { - LogPrintf("%s : failed to create output\n", __func__); - it++; - continue; + if (stakeOutput->CreateReward(*this, indexPrev, txNew)) { + return stakeOutput.get(); } - txNew.vout.insert(txNew.vout.end(), vout.begin(), vout.end()); - - // Set output amount - int outputs = (int) txNew.vout.size() - 1; - CAmount nRemaining = nCredit; - if (outputs > 1) { - // Split the stake across the outputs - CAmount nShare = nRemaining / outputs; - for (int i = 1; i < outputs; i++) { - // loop through all but the last one. - txNew.vout[i].nValue = nShare; - nRemaining -= nShare; - } - } - // put the remaining on the last output (which all into the first if only one output) - txNew.vout[outputs].nValue += nRemaining; + } - // Set coinstake input - txNew.vin.emplace_back(stakeInput.GetTxIn()); + return nullptr; +} - // Limit size - unsigned int nBytes = ::GetSerializeSize(txNew, PROTOCOL_VERSION); - if (nBytes >= DEFAULT_BLOCK_MAX_SIZE / 5) - return error("%s : exceeded coinstake size limit", __func__); +bool CWallet::CreateShieldReward(const CBlockIndex& indexPrev, const CStakeableShieldNote& shieldNote, CMutableTransaction& txNew) +{ + CAmount nMasternodePayment = GetMasternodePayment(indexPrev.nHeight + 1); + TransactionBuilder txBuilder(Params().GetConsensus(), this); + txBuilder.SetFee(0); + txBuilder.AddStakeInput(); + txBuilder.AddSaplingOutput(m_sspk_man->getCommonOVK(), GenerateNewSaplingZKey(), shieldNote.note.value() + GetBlockValue(indexPrev.nHeight + 1) - nMasternodePayment); + libzcash::SaplingExtendedSpendingKey sk; + if (!GetSaplingExtendedSpendingKey(shieldNote.address, sk)) { + return false; + } + + uint256 anchor; + std::vector> witnesses; + std::vector noteop; + noteop.emplace_back(shieldNote.op); + m_sspk_man->GetSaplingNoteWitnesses(noteop, witnesses, anchor); + txBuilder.AddSaplingSpend(sk.expsk, shieldNote.note, anchor, witnesses[0].get()); + + const auto& txTrial = txBuilder.Build().GetTx(); + if (txTrial) { + txNew = CMutableTransaction(*txTrial); + txNew.shieldStakePrivKey = sk.expsk.ask; + txNew.shieldStakeRandomness = txBuilder.GetShieldStakeRandomness(); + return true; + } else { + return false; + } +} + +bool CWallet::CreateTransparentReward(const CBlockIndex& indexPrev, const CStakeableOutput& stakeOutput, CMutableTransaction& txNew) +{ + const auto& stakeInput = stakeOutput.ToInput(); + // Create the output transaction(s) + std::vector vout; + CAmount nMasternodePayment = GetMasternodePayment(indexPrev.nHeight + 1); + CAmount nCredit = stakeOutput.Value() + GetBlockValue(indexPrev.nHeight + 1); - break; + if (!CreateCoinstakeOuts(*stakeInput, vout, nCredit - nMasternodePayment)) { + LogPrintf("%s : failed to create output\n", __func__); + return false; } - LogPrint(BCLog::STAKING, "%s: attempted staking %d times\n", __func__, nAttempts); + txNew.vout.insert(txNew.vout.end(), vout.begin(), vout.end()); + + // Set output amount + int outputs = (int)txNew.vout.size() - 1; + CAmount nRemaining = nCredit; + if (outputs > 1) { + // Split the stake across the outputs + CAmount nShare = nRemaining / outputs; + for (int i = 1; i < outputs; i++) { + // loop through all but the last one. + txNew.vout[i].nValue = nShare; + nRemaining -= nShare; + } + } + // put the remaining on the last output (which all into the first if only one output) + txNew.vout[outputs].nValue += nRemaining; - return fKernelFound; + // Set coinstake input + txNew.vin.emplace_back(stakeInput->GetTxIn()); + return true; } bool CWallet::SignCoinStake(CMutableTransaction& txNew) const @@ -4939,9 +5018,8 @@ const CWDestination* CAddressBookIterator::GetDestKey() } CStakeableOutput::CStakeableOutput(const CWalletTx* txIn, - int iIn, - int nDepthIn, - const CBlockIndex*& _pindex) : - COutput(txIn, iIn, nDepthIn, true /*fSpendable*/, true/*fSolvable*/, true/*fSafe*/), - pindex(_pindex) + int iIn, + int nDepthIn, + const CBlockIndex*& _pindex) : COutput(txIn, iIn, nDepthIn, true /*fSpendable*/, true /*fSolvable*/, true /*fSafe*/), + pindex(_pindex) {} diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index b85b3e287b2fd..1355cf683a399 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -14,6 +14,7 @@ #include "consensus/validation.h" #include "crypter.h" #include "destination_io.h" +#include "guiinterface.h" #include "kernel.h" #include "key.h" #include "key_io.h" @@ -23,14 +24,15 @@ #include "primitives/block.h" #include "primitives/transaction.h" #include "sapling/address.h" -#include "guiinterface.h" +#include "sapling/saplingscriptpubkeyman.h" +#include "sapling/transaction_builder.h" +#include "script/ismine.h" +#include "stakeinput.h" #include "util/system.h" #include "utilstrencodings.h" +#include "validation.h" #include "validationinterface.h" -#include "script/ismine.h" #include "wallet/scriptpubkeyman.h" -#include "sapling/saplingscriptpubkeyman.h" -#include "validation.h" #include "wallet/walletdb.h" #include @@ -99,6 +101,8 @@ class ScriptPubKeyMan; class SaplingScriptPubKeyMan; class SaplingNoteData; struct SaplingNoteEntry; +class CStakeableInterface; +class CStakeableShieldNote; /** (client) version numbers for particular wallet features */ enum WalletFeature { @@ -835,7 +839,10 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface */ bool SelectCoinsMinConf(const CAmount& nTargetValue, int nConfMine, int nConfTheirs, uint64_t nMaxAncestors, std::vector vCoins, std::set >& setCoinsRet, CAmount& nValueRet) const; //! >> Available coins (staking) - bool StakeableCoins(std::vector* pCoins = nullptr); + bool StakeableCoins(std::vector>* stakeableCoins) const; + bool StakeableNotes(std::vector* stakeableNotes) const; + bool StakeableUTXOs(std::vector* stakeableUTXOs) const; + //! >> Available coins (P2CS) void GetAvailableP2CSCoins(std::vector& vCoins) const; @@ -860,6 +867,7 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface std::string& strError); bool IsSaplingSpent(const SaplingOutPoint& op) const; + bool IsSaplingSpent(const uint256& nullifier) const; bool IsSpent(const COutPoint& outpoint) const; bool IsSpent(const uint256& hash, unsigned int n) const; @@ -1059,6 +1067,7 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface CAmount GetColdStakingBalance() const; // delegated coins for which we have the staking key CAmount GetImmatureColdStakingBalance() const; CAmount GetStakingBalance(const bool fIncludeColdStaking = true) const; + CAmount GetShieldStakingBalance(); CAmount GetDelegatedBalance() const; // delegated coins for which we have the spending key CAmount GetImmatureDelegatedBalance() const; CAmount GetLockedCoins() const; @@ -1110,20 +1119,18 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface CWallet::CommitResult CommitTransaction(CTransactionRef tx, CReserveKey& opReservekey, CConnman* connman); CWallet::CommitResult CommitTransaction(CTransactionRef tx, CReserveKey* reservekey, CConnman* connman, mapValue_t* extraValues=nullptr); - bool CreateCoinstakeOuts(const CPivStake& stakeInput, std::vector& vout, CAmount nTotal) const; - bool CreateCoinStake(const CBlockIndex* pindexPrev, - unsigned int nBits, - CMutableTransaction& txNew, - int64_t& nTxNewTime, - std::vector* availableCoins, - bool stopOnNewBlock = true) const; + bool CreateShieldReward(const CBlockIndex& indexPrev, const CStakeableShieldNote& shieldInput, CMutableTransaction& txNew); + bool CreateTransparentReward(const CBlockIndex& indexPrev, const CStakeableOutput& stakeInput, CMutableTransaction& txNew); + + bool CreateCoinstakeOuts(const CStakeInput& stakeInput, std::vector& vout, CAmount nTotal) const; + CStakeableInterface* CreateCoinStake(const CBlockIndex& indexPrev, unsigned int nBits, CMutableTransaction& txNew, int64_t& nTxNewTime, const std::vector>& stakeOutputs, bool stopOnNewBlock); + bool SignCoinStake(CMutableTransaction& txNew) const; void AutoCombineDust(CConnman* connman); // Shielded balances CAmount GetAvailableShieldedBalance(bool fUseCache = true) const; CAmount GetUnconfirmedShieldedBalance() const; - static CFeeRate minTxFee; size_t KeypoolCountExternalKeys(); @@ -1306,14 +1313,47 @@ class COutput std::string ToString() const; }; -class CStakeableOutput : public COutput +/** Stakeable interface rapresenting an UTXO or unspent note */ +class CStakeableInterface +{ +public: + virtual ~CStakeableInterface() {} + virtual std::unique_ptr ToInput() const = 0; + virtual bool CreateReward(CWallet& wallet, const CBlockIndex& indexPrev, CMutableTransaction& txNew) const = 0; +}; + +class CStakeableOutput : public COutput, public CStakeableInterface { public: const CBlockIndex* pindex{nullptr}; CStakeableOutput(const CWalletTx* txIn, int iIn, int nDepthIn, const CBlockIndex*& pindex); + std::unique_ptr ToInput() const override + { + COutPoint outPoint = COutPoint(tx->GetHash(), this->i); + return std::make_unique(tx->tx->vout[this->i], outPoint, pindex); + } + bool CreateReward(CWallet& wallet, const CBlockIndex& indexPrev, CMutableTransaction& txNew) const override + { + return wallet.CreateTransparentReward(indexPrev, *this, txNew); + } +}; +class CStakeableShieldNote : public SaplingNoteEntry, public CStakeableInterface +{ +public: + uint256 nullifier; + CAmount suggestedValue = 0; + explicit CStakeableShieldNote(const SaplingNoteEntry& _note, uint256 _nullifier) : SaplingNoteEntry(_note), nullifier(_nullifier) {} + std::unique_ptr ToInput() const override + { + return std::make_unique(nullifier, note.value()); + } + bool CreateReward(CWallet& wallet, const CBlockIndex& indexPrev, CMutableTransaction& txNew) const override + { + return wallet.CreateShieldReward(indexPrev, *this, txNew); + } }; /** RAII object to check and reserve a wallet rescan */ diff --git a/src/zpiv/zpos.h b/src/zpiv/zpos.h index 424e9288886f4..17c81a36b37bc 100644 --- a/src/zpiv/zpos.h +++ b/src/zpiv/zpos.h @@ -7,6 +7,7 @@ #include "stakeinput.h" #include "txdb.h" +#include class CLegacyZPivStake : public CStakeInput { @@ -26,11 +27,17 @@ class CLegacyZPivStake : public CStakeInput static CLegacyZPivStake* NewZPivStake(const CTxIn& txin, int nHeight); bool IsZPIV() const override { return true; } + bool IsShieldPIV() const override { return false; }; uint32_t GetChecksum() const { return nChecksum; } const CBlockIndex* GetIndexFrom() const override; CAmount GetValue() const override; CDataStream GetUniqueness() const override; bool GetTxOutFrom(CTxOut& out) const override { return false; /* not available */ } + CTxIn GetTxIn() const override + { + throw "can't be bothered"; + } + std::pair GetSpendInfo() const override { throw new std::runtime_error{"Function non defined for zPiv"}; }; }; #endif //PIVX_LEGACY_ZPOS_H diff --git a/test/functional/mining_pos_coldStaking.py b/test/functional/mining_pos_coldStaking.py index c073da2563cd4..9306d6d90d986 100755 --- a/test/functional/mining_pos_coldStaking.py +++ b/test/functional/mining_pos_coldStaking.py @@ -92,7 +92,7 @@ def run_test(self): assert self.nodes[1].lockunspent(False, True, [{"txid": x['txid'], "vout": x['vout']}]) # check that it cannot stake sleep(1) - assert_equal(self.nodes[1].getstakingstatus()["stakeablecoins"], 0) + assert_equal(self.nodes[1].getstakingstatus()["transparent_stakeable_coins"], 0) # create shielded balance for node 0 self.log.info("Shielding some coins for node0...") self.nodes[0].shieldsendmany("from_transparent", [{"address": self.nodes[0].getnewshieldaddress(), @@ -205,7 +205,7 @@ def run_test(self): # ----------------------------------------------------------- print("*** 7 ***") self.log.info("Trying to generate a cold-stake block before whitelisting the owner...") - assert_equal(self.nodes[1].getstakingstatus()["stakeablecoins"], 0) + assert_equal(self.nodes[1].getstakingstatus()["transparent_stakeable_coins"], 0) assert_equal(self.nodes[1].listdelegators(), []) self.log.info("Nice. Cold staker was NOT able to create the block yet.") @@ -232,7 +232,7 @@ def run_test(self): # 9) check that the staker can use the coins to stake a block with internal miner. # -------------------------------------------------------------------------------- print("*** 9 ***") - assert_equal(self.nodes[1].getstakingstatus()["stakeablecoins"], NUM_OF_INPUTS-1) + assert_equal(self.nodes[1].getstakingstatus()["transparent_stakeable_coins"], NUM_OF_INPUTS-1) self.log.info("Generating one valid cold-stake block...") self.mocktime = self.generate_pos(1, self.mocktime) self.log.info("New block created by cold-staking. Trying to submit...") @@ -338,12 +338,12 @@ def run_test(self): assert ret assert_equal(self.nodes[1].listdelegators(), []) assert_equal(self.nodes[1].listdelegators(True)[0]["address"], owner_address) - assert_equal(self.nodes[1].getstakingstatus()["stakeablecoins"], 0) + assert_equal(self.nodes[1].getstakingstatus()["transparent_stakeable_coins"], 0) self.log.info("Re-enable delegation") ret = self.nodes[1].delegatoradd(owner_address) assert ret assert_equal(self.nodes[1].listdelegators()[0]["address"], owner_address) - assert_equal(self.nodes[1].getstakingstatus()["stakeablecoins"], len(stakeable_coins)) + assert_equal(self.nodes[1].getstakingstatus()["transparent_stakeable_coins"], len(stakeable_coins)) self.log.info("Cancel the stake delegation spending the delegated utxos...") delegated_utxos = getDelegatedUtxos(self.nodes[0].listunspent()) # remove one utxo to spend later @@ -379,7 +379,7 @@ def run_test(self): # ----------------------------------------------------------- print("*** 14 ***") self.log.info("Trying to generate one cold-stake block again...") - assert_equal(self.nodes[1].getstakingstatus()["stakeablecoins"], 0) + assert_equal(self.nodes[1].getstakingstatus()["transparent_stakeable_coins"], 0) self.log.info("Good. Cold staker was NOT able to create any more blocks.") # 15) check balances when mature. diff --git a/test/functional/mining_shield_pos.py b/test/functional/mining_shield_pos.py new file mode 100755 index 0000000000000..b5b189e01855f --- /dev/null +++ b/test/functional/mining_shield_pos.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +# Copyright (c) 2023 The PIVX Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +from test_framework.test_framework import PivxTestFramework +from test_framework.authproxy import JSONRPCException +from test_framework.util import ( + assert_equal, + assert_greater_than, + assert_raises_rpc_error, + disconnect_nodes, +) +import time + + +class PIVX_ShieldStakingTest(PivxTestFramework): + + def generate_shield_pos(self, node_id): + assert_greater_than(len(self.nodes), node_id) + rpc_conn = self.nodes[node_id] + ss = rpc_conn.getstakingstatus() + assert ss["walletunlocked"] + assert ss["shield_stakeables_notes"] > 0 + assert ss["shield_staking_balance"] > 0.0 + fStaked = False + failures = 0 + while not fStaked: + try: + res = rpc_conn.generate(1) + fStaked = True + return res + except JSONRPCException as e: + if ("Couldn't create new block" in str(e)): + failures += 1 + # couldn't generate block. check that this node can still stake (after 60 failures) + if failures > 60: + ss = rpc_conn.getstakingstatus() + if not (ss["walletunlocked"] and ss["shield_stakeables_notes"] > 0 and ss["shield_staking_balance"] > 0.0): + raise AssertionError("Node %d unable to stake!" % node_id) + # try to stake one sec in the future + time.sleep(1) + else: + raise e + + def set_test_params(self): + self.num_nodes = 3 + self.setup_clean_chain = True + + def run_test(self): + self.log.info("Shield stake functional tests") + + # Mine enough blocks to activate shield staking" + a bonus so node0 has enough pivs to mine the final block + self.nodes[0].generate(800) + self.sync_all() + + # Send some shielded notes to node1 + saplingAddr1 = self.nodes[1].getnewshieldaddress() + txid = self.nodes[0].sendtoaddress(saplingAddr1, 40000, "", "", True) + + # Generate more blocks so the shield notes becomes stakeable + self.nodes[0].generate(20) + self.sync_all() + + # Sanity check, shield staking is activated + assert_equal(self.nodes[0].getblockchaininfo()['upgrades']['v5 shield']['status'], 'active') + assert_equal(self.nodes[0].getblockchaininfo()['upgrades']['v6 evo']['status'], 'active') + assert_equal(self.nodes[0].getblockchaininfo()['upgrades']['shield staking']['status'], 'active') + + # Node1 has exactly one shield stakeable note + assert_equal(self.nodes[1].getstakingstatus()['shield_stakeables_notes'], 1) + assert_equal(self.nodes[1].getstakingstatus()['transparent_stakeable_coins'], 0) + + # Before generating the shield stake block isolate node 2: + disconnect_nodes(self.nodes[0], 2) + disconnect_nodes(self.nodes[2], 0) + disconnect_nodes(self.nodes[1], 2) + disconnect_nodes(self.nodes[2], 1) + + # Generate the block and check that reward is 4 shield PIVs + initialBalance = self.nodes[1].getshieldbalance() + shieldStakeBlockHash = self.generate_shield_pos(1)[0] + assert_equal(self.nodes[1].getshieldbalance()-initialBalance, 4) + + # Check that a loose coinShieldStakeTx cannot go in mempool + coinShieldStakeTxHash = self.nodes[1].getblock(shieldStakeBlockHash)['tx'][1] + coinShieldStakeTxHex = self.nodes[1].gettransaction(coinShieldStakeTxHash)['hex'] + assert_raises_rpc_error(-26, "coinshieldstake", self.nodes[2].sendrawtransaction, coinShieldStakeTxHex) + + # Check that node0 accepted the block: + self.sync_all(self.nodes[0:2]) + assert_equal(self.nodes[1].getblockhash(821), shieldStakeBlockHash) + assert_equal(self.nodes[0].getblockhash(821), shieldStakeBlockHash) + + # Finally verify that the reward can be spent: + recipient = [{"address": self.nodes[0].getnewshieldaddress(), "amount": (initialBalance+2)}] + txid = self.nodes[1].shieldsendmany("from_shield", recipient) + self.sync_all(self.nodes[0:2]) + blockHash = self.nodes[0].generate(1)[0] + self.sync_all(self.nodes[0:2]) + assert (txid in self.nodes[0].getblock(blockHash)['tx']) + assert_equal(self.nodes[1].getblockhash(822), blockHash) + + +if __name__ == '__main__': + PIVX_ShieldStakingTest().main() diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index 99067f6988c82..20ef615a8df68 100755 --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -886,8 +886,8 @@ def generate_pos(self, node_id, btime=None): rpc_conn = self.nodes[node_id] ss = rpc_conn.getstakingstatus() assert ss["walletunlocked"] - assert ss["stakeablecoins"] > 0 - assert ss["stakingbalance"] > 0.0 + assert ss["transparent_stakeable_coins"] > 0 + assert ss["transparent_staking_balance"] > 0.0 if btime is not None: next_btime = btime + 60 fStaked = False @@ -902,7 +902,7 @@ def generate_pos(self, node_id, btime=None): # couldn't generate block. check that this node can still stake (after 60 failures) if failures > 60: ss = rpc_conn.getstakingstatus() - if not (ss["walletunlocked"] and ss["stakeablecoins"] > 0 and ss["stakingbalance"] > 0.0): + if not (ss["walletunlocked"] and ss["transparent_stakeable_coins"] > 0 and ss["transparent_staking_balance"] > 0.0): raise AssertionError("Node %d unable to stake!" % node_id) # try to stake one sec in the future if btime is not None: diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index bcec57836aecb..0cddb4bf9569a 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -94,6 +94,7 @@ 'rpc_bind.py --nonloopback', # ~ 126 sec 'feature_uacomment.py', # ~ 125 sec 'interface_rest.py', # ~ 120 sec + 'mining_shield_pos.py', # ~ 120 sec # vv Tests less than 2m vv 'wallet_upgrade.py', # ~ 119 sec @@ -220,6 +221,7 @@ 'mempool_reorg.py', 'mempool_resurrect.py', 'mempool_spend_coinbase.py', + 'mining_shield_pos.py', 'p2p_disconnect_ban.py', 'p2p_time_offset.py', 'rpc_bip38.py', diff --git a/test/lint/lint-circular-dependencies.sh b/test/lint/lint-circular-dependencies.sh index 7d984d21df94a..06bea50702654 100755 --- a/test/lint/lint-circular-dependencies.sh +++ b/test/lint/lint-circular-dependencies.sh @@ -55,6 +55,7 @@ EXPECTED_CIRCULAR_DEPENDENCIES=( "chain -> legacy/stakemodifier -> validation -> pow -> chain" "evo/deterministicmns -> masternodeman -> net -> tiertwo/net_masternodes -> evo/deterministicmns" "evo/deterministicmns -> masternodeman -> validation -> validationinterface -> evo/deterministicmns" + "blocksignature -> sapling/transaction_builder -> validation -> blocksignature" ) EXIT_CODE=0