diff --git a/qa/pull-tester/rpc-tests.sh b/qa/pull-tester/rpc-tests.sh index d4f628428..b0219b4d2 100755 --- a/qa/pull-tester/rpc-tests.sh +++ b/qa/pull-tester/rpc-tests.sh @@ -81,6 +81,7 @@ testScripts=( 'shorter_block_times.py' 'sprout_sapling_migration.py' 'turnstile.py' + 'mining_shielded_coinbase.py' ); testScriptsExt=( 'getblocktemplate_longpoll.py' diff --git a/qa/rpc-tests/mining_shielded_coinbase.py b/qa/rpc-tests/mining_shielded_coinbase.py new file mode 100755 index 000000000..c6e44d000 --- /dev/null +++ b/qa/rpc-tests/mining_shielded_coinbase.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +# Copyright (c) 2019 The Zcash developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or https://www.opensource.org/licenses/mit-license.php . + +from decimal import Decimal +from test_framework.authproxy import JSONRPCException +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, + assert_raises, + bitcoind_processes, + connect_nodes, + initialize_chain_clean, + start_node, + wait_and_assert_operationid_status, +) + +class ShieldCoinbaseTest (BitcoinTestFramework): + + def setup_chain(self): + print("Initializing test directory "+self.options.tmpdir) + initialize_chain_clean(self.options.tmpdir, 4) + + def start_node_with(self, index, extra_args=[]): + args = [ + "-nuparams=2bb40e60:1", # Blossom + "-nuparams=f5b9230b:10", # Heartwood + "-nurejectoldversions=false", + ] + return start_node(index, self.options.tmpdir, args + extra_args) + + def setup_network(self, split=False): + self.nodes = [] + self.nodes.append(self.start_node_with(0)) + self.nodes.append(self.start_node_with(1)) + connect_nodes(self.nodes[1], 0) + self.is_network_split=False + self.sync_all() + + def run_test (self): + # Generate a Sapling address for node 1 + node1_zaddr = self.nodes[1].z_getnewaddress('sapling') + + self.nodes[1].stop() + bitcoind_processes[1].wait() + self.nodes[1] = self.start_node_with(1, [ + "-mineraddress=%s" % node1_zaddr, + ]) + connect_nodes(self.nodes[1], 0) + + # Node 0 can mine blocks, because it is targeting a transparent address + print("Mining block with node 0") + self.nodes[0].generate(1) + self.sync_all() + walletinfo = self.nodes[0].getwalletinfo() + assert_equal(walletinfo['immature_balance'], 5) + assert_equal(walletinfo['balance'], 0) + + # Node 1 cannot mine blocks, because it is targeting a Sapling address + # but Heartwood is not yet active + print("Attempting to mine block with node 1") + assert_raises(JSONRPCException, self.nodes[1].generate, 1) + assert_equal(self.nodes[1].z_getbalance(node1_zaddr, 0), 0) + assert_equal(self.nodes[1].z_getbalance(node1_zaddr), 0) + + # Stop node 1 and check logs to verify the block was rejected correctly + print("Checking node 1 logs") + self.nodes[1].stop() + bitcoind_processes[1].wait() + logpath = self.options.tmpdir + "/node1/regtest/debug.log" + foundErrorMsg = False + with open(logpath, "r") as myfile: + logdata = myfile.readlines() + for logline in logdata: + if "CheckTransaction(): coinbase has output descriptions" in logline: + foundErrorMsg = True + break + assert(foundErrorMsg) + + # Restart node 1 + self.nodes[1] = self.start_node_with(1, [ + "-mineraddress=%s" % node1_zaddr, + ]) + connect_nodes(self.nodes[1], 0) + + # Activate Heartwood + print("Activating Heartwood") + self.nodes[0].generate(8) + self.sync_all() + + # Node 1 can now mine blocks! + print("Mining block with node 1") + self.nodes[1].generate(1) + self.sync_all() + + # Transparent coinbase outputs are subject to coinbase maturity + assert_equal(self.nodes[0].getbalance(), Decimal('0')) + assert_equal(self.nodes[0].z_gettotalbalance()['transparent'], '0.00') + assert_equal(self.nodes[0].z_gettotalbalance()['private'], '0.00') + assert_equal(self.nodes[0].z_gettotalbalance()['total'], '0.00') + + # Shielded coinbase outputs are not subject to coinbase maturity + assert_equal(self.nodes[1].z_getbalance(node1_zaddr, 0), 5) + assert_equal(self.nodes[1].z_getbalance(node1_zaddr), 5) + assert_equal(self.nodes[1].z_gettotalbalance()['private'], '5.00') + assert_equal(self.nodes[1].z_gettotalbalance()['total'], '5.00') + + # Send from Sapling coinbase to Sapling address and transparent address + # (to check that a non-empty vout is allowed when spending shielded + # coinbase) + print("Sending Sapling coinbase to Sapling address") + node0_zaddr = self.nodes[0].z_getnewaddress('sapling') + node0_taddr = self.nodes[0].getnewaddress() + recipients = [] + recipients.append({"address": node0_zaddr, "amount": Decimal('2')}) + recipients.append({"address": node0_taddr, "amount": Decimal('2')}) + myopid = self.nodes[1].z_sendmany(node1_zaddr, recipients, 1, 0) + wait_and_assert_operationid_status(self.nodes[1], myopid) + self.sync_all() + self.nodes[0].generate(1) + self.sync_all() + + assert_equal(self.nodes[0].z_getbalance(node0_zaddr), 2) + assert_equal(self.nodes[0].z_getbalance(node0_taddr), 2) + assert_equal(self.nodes[1].z_getbalance(node1_zaddr), 1) + +if __name__ == '__main__': + ShieldCoinbaseTest().main() diff --git a/src/gtest/test_checktransaction.cpp b/src/gtest/test_checktransaction.cpp index 3279263fc..0d1c96920 100644 --- a/src/gtest/test_checktransaction.cpp +++ b/src/gtest/test_checktransaction.cpp @@ -5,8 +5,11 @@ #include "main.h" #include "primitives/transaction.h" #include "consensus/validation.h" +#include "transaction_builder.h" #include "utiltest.h" +#include + extern ZCJoinSplit* params; TEST(ChecktransactionTests, CheckVpubNotBothNonzero) { @@ -1027,3 +1030,191 @@ TEST(ChecktransactionTests, BadTxReceivedOverNetwork) } } } + +TEST(CheckTransaction, InvalidShieldedCoinbase) { + RegtestActivateSapling(); + + CMutableTransaction mtx = GetValidTransaction(); + mtx.fOverwintered = true; + mtx.nVersionGroupId = SAPLING_VERSION_GROUP_ID; + mtx.nVersion = SAPLING_TX_VERSION; + + // Make it an invalid shielded coinbase (no ciphertexts or commitments). + mtx.vin.resize(1); + mtx.vin[0].prevout.SetNull(); + mtx.vShieldedOutput.resize(1); + mtx.vJoinSplit.resize(0); + + CTransaction tx(mtx); + EXPECT_TRUE(tx.IsCoinBase()); + + // Before Heartwood, output descriptions are rejected. + MockCValidationState state; + EXPECT_CALL(state, DoS(100, false, REJECT_INVALID, "bad-cb-has-output-description", false)).Times(1); + ContextualCheckTransaction(tx, state, Params(), 10, 57); + + RegtestActivateHeartwood(false, Consensus::NetworkUpgrade::ALWAYS_ACTIVE); + + // From Heartwood, the output description is allowed but invalid (undecryptable). + EXPECT_CALL(state, DoS(100, false, REJECT_INVALID, "bad-cb-output-desc-invalid-outct", false)).Times(1); + ContextualCheckTransaction(tx, state, Params(), 10, 57); + + RegtestDeactivateHeartwood(); +} + +TEST(CheckTransaction, HeartwoodAcceptsShieldedCoinbase) { + RegtestActivateHeartwood(false, Consensus::NetworkUpgrade::ALWAYS_ACTIVE); + auto chainparams = Params(); + + uint256 ovk; + auto note = libzcash::SaplingNote( + libzcash::SaplingSpendingKey::random().default_address(), CAmount(123456)); + auto output = OutputDescriptionInfo(ovk, note, {{0xF6}}); + + auto ctx = librustzcash_sapling_proving_ctx_init(); + auto odesc = output.Build(ctx).get(); + librustzcash_sapling_proving_ctx_free(ctx); + + CMutableTransaction mtx = GetValidTransaction(); + mtx.fOverwintered = true; + mtx.nVersionGroupId = SAPLING_VERSION_GROUP_ID; + mtx.nVersion = SAPLING_TX_VERSION; + + mtx.vin.resize(1); + mtx.vin[0].prevout.SetNull(); + mtx.vJoinSplit.resize(0); + mtx.vShieldedOutput.push_back(odesc); + + // Transaction should fail with a bad public cmu. + { + auto cmOrig = mtx.vShieldedOutput[0].cmu; + mtx.vShieldedOutput[0].cmu = uint256S("1234"); + CTransaction tx(mtx); + EXPECT_TRUE(tx.IsCoinBase()); + + MockCValidationState state; + EXPECT_CALL(state, DoS(100, false, REJECT_INVALID, "bad-cb-output-desc-invalid-outct", false)).Times(1); + ContextualCheckTransaction(tx, state, chainparams, 10, 57); + + mtx.vShieldedOutput[0].cmu = cmOrig; + } + + // Transaction should fail with a bad outCiphertext. + { + auto outCtOrig = mtx.vShieldedOutput[0].outCiphertext; + mtx.vShieldedOutput[0].outCiphertext = {{}}; + CTransaction tx(mtx); + EXPECT_TRUE(tx.IsCoinBase()); + + MockCValidationState state; + EXPECT_CALL(state, DoS(100, false, REJECT_INVALID, "bad-cb-output-desc-invalid-outct", false)).Times(1); + ContextualCheckTransaction(tx, state, chainparams, 10, 57); + + mtx.vShieldedOutput[0].outCiphertext = outCtOrig; + } + + // Transaction should fail with a bad encCiphertext. + { + auto encCtOrig = mtx.vShieldedOutput[0].encCiphertext; + mtx.vShieldedOutput[0].encCiphertext = {{}}; + CTransaction tx(mtx); + EXPECT_TRUE(tx.IsCoinBase()); + + MockCValidationState state; + EXPECT_CALL(state, DoS(100, false, REJECT_INVALID, "bad-cb-output-desc-invalid-encct", false)).Times(1); + ContextualCheckTransaction(tx, state, chainparams, 10, 57); + + mtx.vShieldedOutput[0].encCiphertext = encCtOrig; + } + + // Test the success case. The unmodified transaction should fail signature + // validation, which is an unrelated consensus rule and is checked after the + // shielded coinbase rules have passed. + { + CTransaction tx(mtx); + EXPECT_TRUE(tx.IsCoinBase()); + + MockCValidationState state; + EXPECT_CALL(state, DoS(100, false, REJECT_INVALID, "bad-txns-sapling-binding-signature-invalid", false)).Times(1); + ContextualCheckTransaction(tx, state, chainparams, 10, 57); + } + + RegtestDeactivateHeartwood(); +} + +// Check that the consensus rules relevant to valueBalance, vShieldedOutput, and +// bindingSig from https://zips.z.cash/protocol/protocol.pdf#txnencoding are +// applied to coinbase transactions. +TEST(CheckTransaction, HeartwoodEnforcesSaplingRulesOnShieldedCoinbase) { + RegtestActivateHeartwood(false, Consensus::NetworkUpgrade::ALWAYS_ACTIVE); + auto chainparams = Params(); + + uint256 ovk; + auto note = libzcash::SaplingNote( + libzcash::SaplingSpendingKey::random().default_address(), CAmount(123456)); + auto output = OutputDescriptionInfo(ovk, note, {{0xF6}}); + + CMutableTransaction mtx = GetValidTransaction(); + mtx.fOverwintered = true; + mtx.nVersionGroupId = SAPLING_VERSION_GROUP_ID; + mtx.nVersion = SAPLING_TX_VERSION; + + mtx.vin.resize(1); + mtx.vin[0].prevout.SetNull(); + mtx.vin[0].scriptSig << 123; + mtx.vJoinSplit.resize(0); + mtx.valueBalance = -1000; + + // Coinbase transaction should fail non-contextual checks with no shielded + // outputs and non-zero valueBalance. + { + CTransaction tx(mtx); + EXPECT_TRUE(tx.IsCoinBase()); + + MockCValidationState state; + EXPECT_CALL(state, DoS(100, false, REJECT_INVALID, "bad-txns-valuebalance-nonzero", false)).Times(1); + EXPECT_FALSE(CheckTransactionWithoutProofVerification(tx, state)); + } + + // Add a Sapling output. + auto ctx = librustzcash_sapling_proving_ctx_init(); + auto odesc = output.Build(ctx).get(); + librustzcash_sapling_proving_ctx_free(ctx); + mtx.vShieldedOutput.push_back(odesc); + + // Coinbase transaction should fail non-contextual checks with valueBalance + // out of range. + { + mtx.valueBalance = MAX_MONEY + 1; + CTransaction tx(mtx); + EXPECT_TRUE(tx.IsCoinBase()); + + MockCValidationState state; + EXPECT_CALL(state, DoS(100, false, REJECT_INVALID, "bad-txns-valuebalance-toolarge", false)).Times(1); + EXPECT_FALSE(CheckTransactionWithoutProofVerification(tx, state)); + } + { + mtx.valueBalance = -MAX_MONEY - 1; + CTransaction tx(mtx); + EXPECT_TRUE(tx.IsCoinBase()); + + MockCValidationState state; + EXPECT_CALL(state, DoS(100, false, REJECT_INVALID, "bad-txns-valuebalance-toolarge", false)).Times(1); + EXPECT_FALSE(CheckTransactionWithoutProofVerification(tx, state)); + } + + mtx.valueBalance = -1000; + CTransaction tx(mtx); + EXPECT_TRUE(tx.IsCoinBase()); + + // Coinbase transaction should now pass non-contextual checks. + MockCValidationState state; + EXPECT_TRUE(CheckTransactionWithoutProofVerification(tx, state)); + + // Coinbase transaction does not pass contextual checks, as bindingSig + // consensus rule is enforced. + EXPECT_CALL(state, DoS(100, false, REJECT_INVALID, "bad-txns-sapling-binding-signature-invalid", false)).Times(1); + ContextualCheckTransaction(tx, state, chainparams, 10, 57); + + RegtestDeactivateHeartwood(); +} diff --git a/src/gtest/test_miner.cpp b/src/gtest/test_miner.cpp index 695f991b3..f00781410 100644 --- a/src/gtest/test_miner.cpp +++ b/src/gtest/test_miner.cpp @@ -7,37 +7,37 @@ #include "util.h" -TEST(Miner, GetScriptForMinerAddress) { +TEST(Miner, GetMinerAddress) { SelectParams(CBaseChainParams::MAIN); // No miner address set { - boost::shared_ptr coinbaseScript; - GetScriptForMinerAddress(coinbaseScript); - EXPECT_FALSE((bool) coinbaseScript); + MinerAddress minerAddress; + GetMinerAddress(minerAddress); + EXPECT_FALSE(IsValidMinerAddress(minerAddress)); } mapArgs["-mineraddress"] = "notAnAddress"; { - boost::shared_ptr coinbaseScript; - GetScriptForMinerAddress(coinbaseScript); - EXPECT_FALSE((bool) coinbaseScript); + MinerAddress minerAddress; + GetMinerAddress(minerAddress); + EXPECT_FALSE(IsValidMinerAddress(minerAddress)); } - // Partial address + // Partial transparent address mapArgs["-mineraddress"] = "t1T8yaLVhNqxA5KJcmiqq"; { - boost::shared_ptr coinbaseScript; - GetScriptForMinerAddress(coinbaseScript); - EXPECT_FALSE((bool) coinbaseScript); + MinerAddress minerAddress; + GetMinerAddress(minerAddress); + EXPECT_FALSE(IsValidMinerAddress(minerAddress)); } - // Typo in address + // Typo in transparent address mapArgs["-mineraddress"] = "t1TByaLVhNqxA5KJcmiqqFN88e8DNp2PBfF"; { - boost::shared_ptr coinbaseScript; - GetScriptForMinerAddress(coinbaseScript); - EXPECT_FALSE((bool) coinbaseScript); + MinerAddress minerAddress; + GetMinerAddress(minerAddress); + EXPECT_FALSE(IsValidMinerAddress(minerAddress)); } // Set up expected scriptPubKey for t1T8yaLVhNqxA5KJcmiqqFN88e8DNp2PBfF @@ -45,30 +45,77 @@ TEST(Miner, GetScriptForMinerAddress) { keyID.SetHex("eb88f1c65b39a823479ac9c7db2f4a865960a165"); CScript expectedCoinbaseScript = CScript() << OP_DUP << OP_HASH160 << ToByteVector(keyID) << OP_EQUALVERIFY << OP_CHECKSIG; - // Valid address + // Valid transparent address mapArgs["-mineraddress"] = "t1T8yaLVhNqxA5KJcmiqqFN88e8DNp2PBfF"; { - boost::shared_ptr coinbaseScript; - GetScriptForMinerAddress(coinbaseScript); - EXPECT_TRUE((bool) coinbaseScript); + MinerAddress minerAddress; + GetMinerAddress(minerAddress); + EXPECT_TRUE(IsValidMinerAddress(minerAddress)); + EXPECT_TRUE(boost::get>(&minerAddress) != nullptr); + auto coinbaseScript = boost::get>(minerAddress); EXPECT_EQ(expectedCoinbaseScript, coinbaseScript->reserveScript); } - // Valid address with leading whitespace + // Valid transparent address with leading whitespace mapArgs["-mineraddress"] = " t1T8yaLVhNqxA5KJcmiqqFN88e8DNp2PBfF"; { - boost::shared_ptr coinbaseScript; - GetScriptForMinerAddress(coinbaseScript); - EXPECT_TRUE((bool) coinbaseScript); + MinerAddress minerAddress; + GetMinerAddress(minerAddress); + EXPECT_TRUE(IsValidMinerAddress(minerAddress)); + EXPECT_TRUE(boost::get>(&minerAddress) != nullptr); + auto coinbaseScript = boost::get>(minerAddress); EXPECT_EQ(expectedCoinbaseScript, coinbaseScript->reserveScript); } - // Valid address with trailing whitespace + // Valid transparent address with trailing whitespace mapArgs["-mineraddress"] = "t1T8yaLVhNqxA5KJcmiqqFN88e8DNp2PBfF "; { - boost::shared_ptr coinbaseScript; - GetScriptForMinerAddress(coinbaseScript); - EXPECT_TRUE((bool) coinbaseScript); + MinerAddress minerAddress; + GetMinerAddress(minerAddress); + EXPECT_TRUE(IsValidMinerAddress(minerAddress)); + EXPECT_TRUE(boost::get>(&minerAddress) != nullptr); + auto coinbaseScript = boost::get>(minerAddress); EXPECT_EQ(expectedCoinbaseScript, coinbaseScript->reserveScript); } + + // Partial Sapling address + mapArgs["-mineraddress"] = "zs1z7rejlpsa98s2rrrfkwmaxu53"; + { + MinerAddress minerAddress; + GetMinerAddress(minerAddress); + EXPECT_FALSE(IsValidMinerAddress(minerAddress)); + } + + // Typo in Sapling address + mapArgs["-mineraddress"] = "zs1s7rejlpsa98s2rrrfkwmaxu53e4ue0ulcrw0h4x5g8jl04tak0d3mm47vdtahatqrlkngh9slya"; + { + MinerAddress minerAddress; + GetMinerAddress(minerAddress); + EXPECT_FALSE(IsValidMinerAddress(minerAddress)); + } + + // Valid Sapling address + mapArgs["-mineraddress"] = "zs1z7rejlpsa98s2rrrfkwmaxu53e4ue0ulcrw0h4x5g8jl04tak0d3mm47vdtahatqrlkngh9slya"; + { + MinerAddress minerAddress; + GetMinerAddress(minerAddress); + EXPECT_TRUE(IsValidMinerAddress(minerAddress)); + EXPECT_TRUE(boost::get(&minerAddress) != nullptr); + } + + // Valid Sapling address with leading whitespace + mapArgs["-mineraddress"] = " zs1z7rejlpsa98s2rrrfkwmaxu53e4ue0ulcrw0h4x5g8jl04tak0d3mm47vdtahatqrlkngh9slya"; + { + MinerAddress minerAddress; + GetMinerAddress(minerAddress); + EXPECT_FALSE(IsValidMinerAddress(minerAddress)); + } + + // Valid Sapling address with trailing whitespace + mapArgs["-mineraddress"] = "zs1z7rejlpsa98s2rrrfkwmaxu53e4ue0ulcrw0h4x5g8jl04tak0d3mm47vdtahatqrlkngh9slya "; + { + MinerAddress minerAddress; + GetMinerAddress(minerAddress); + EXPECT_FALSE(IsValidMinerAddress(minerAddress)); + } } diff --git a/src/gtest/test_transaction_builder.cpp b/src/gtest/test_transaction_builder.cpp index cd0680d9e..d97d0287e 100644 --- a/src/gtest/test_transaction_builder.cpp +++ b/src/gtest/test_transaction_builder.cpp @@ -94,7 +94,7 @@ TEST(TransactionBuilder, TransparentToSapling) // Create a shielding transaction from transparent to Sapling // 0.0005 t-ZEC in, 0.0004 z-ZEC out, 0.0001 t-ZEC fee auto builder = TransactionBuilder(consensusParams, 1, &keystore); - builder.AddTransparentInput(COutPoint(), scriptPubKey, 50000); + builder.AddTransparentInput(COutPoint(uint256S("1234"), 0), scriptPubKey, 50000); builder.AddSaplingOutput(fvk_from.ovk, pk, 40000, {}); auto tx = builder.Build().GetTxOrThrow(); diff --git a/src/init.cpp b/src/init.cpp index 3acc97285..214bcf2d1 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -437,6 +437,7 @@ std::string HelpMessage(HelpMessageMode mode) strUsage += HelpMessageOpt("-fuzzmessagestest=", "Randomly fuzz 1 of every network messages"); strUsage += HelpMessageOpt("-stopafterblockimport", strprintf("Stop running after importing blocks from disk (default: %u)", DEFAULT_STOPAFTERBLOCKIMPORT)); strUsage += HelpMessageOpt("-nuparams=hexBranchId:activationHeight", "Use given activation height for specified network upgrade (regtest-only)"); + strUsage += HelpMessageOpt("-nurejectoldversions", strprintf("Reject peers that don't know about the current epoch (regtest-only) (default: %u)", DEFAULT_NU_REJECT_OLD_VERSIONS)); } string debugCategories = "addrman, alert, bench, coindb, db, estimatefee, http, libevent, lock, mempool, net, partitioncheck, pow, proxy, prune, " "rand, reindex, rpc, selectcoins, tor, zmq, zrpc, zrpcunsafe (implies zrpc)"; // Don't translate these @@ -1037,9 +1038,15 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler) if (mapArgs.count("-mineraddress")) { CTxDestination addr = DecodeDestination(mapArgs["-mineraddress"]); if (!IsValidDestination(addr)) { - return InitError(strprintf( - _("Invalid address for -mineraddress=: '%s' (must be a transparent address)"), - mapArgs["-mineraddress"])); + // Try a Sapling address + auto zaddr = DecodePaymentAddress(mapArgs["-mineraddress"]); + if (!IsValidPaymentAddress(zaddr) || + boost::get(&zaddr) == nullptr) + { + return InitError(strprintf( + _("Invalid address for -mineraddress=: '%s' (must be a Sapling or transparent address)"), + mapArgs["-mineraddress"])); + } } } #endif @@ -1077,6 +1084,12 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler) } } + if (mapArgs.count("-nurejectoldversions")) { + if (Params().NetworkIDString() != "regtest") { + return InitError("-nurejectoldversions may only be set on regtest."); + } + } + // ********************************************************* Step 4: application initialization: dir lock, daemonize, pidfile, debug log // Initialize libsodium @@ -1517,10 +1530,15 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler) #ifdef ENABLE_WALLET bool minerAddressInLocalWallet = false; if (pwalletMain) { - // Address has already been validated CTxDestination addr = DecodeDestination(mapArgs["-mineraddress"]); - CKeyID keyID = boost::get(addr); - minerAddressInLocalWallet = pwalletMain->HaveKey(keyID); + if (IsValidDestination(addr)) { + CKeyID keyID = boost::get(addr); + minerAddressInLocalWallet = pwalletMain->HaveKey(keyID); + } else { + auto zaddr = DecodePaymentAddress(mapArgs["-mineraddress"]); + minerAddressInLocalWallet = boost::apply_visitor( + HaveSpendingKeyForPaymentAddress(pwalletMain), zaddr); + } } if (GetBoolArg("-minetolocalwallet", true) && !minerAddressInLocalWallet) { return InitError(_("-mineraddress is not in the local wallet. Either use a local address, or set -minetolocalwallet=0")); @@ -1529,19 +1547,20 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler) // This is leveraging the fact that boost::signals2 executes connected // handlers in-order. Further up, the wallet is connected to this signal - // if the wallet is enabled. The wallet's ScriptForMining handler does - // nothing if -mineraddress is set, and GetScriptForMinerAddress() does - // nothing if -mineraddress is not set (or set to an invalid address). + // if the wallet is enabled. The wallet's AddressForMining handler does + // nothing if -mineraddress is set, and GetMinerAddress() does nothing + // if -mineraddress is not set (or set to an address that is not valid + // for mining). // - // The upshot is that when ScriptForMining(script) is called: + // The upshot is that when AddressForMining(address) is called: // - If -mineraddress is set (whether or not the wallet is enabled), the - // CScript argument is set to -mineraddress. - // - If the wallet is enabled and -mineraddress is not set, the CScript - // argument is set to a wallet address. - // - If the wallet is disabled and -mineraddress is not set, the CScript + // argument is set to -mineraddress. + // - If the wallet is enabled and -mineraddress is not set, the argument + // is set to a wallet address. + // - If the wallet is disabled and -mineraddress is not set, the // argument is not modified; in practice this means it is empty, and // GenerateBitcoins() returns an error. - GetMainSignals().ScriptForMining.connect(GetScriptForMinerAddress); + GetMainSignals().AddressForMining.connect(GetMinerAddress); } #endif // ENABLE_MINING diff --git a/src/main.cpp b/src/main.cpp index 5669f9271..042589302 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -791,6 +791,7 @@ bool ContextualCheckTransaction( bool overwinterActive = chainparams.GetConsensus().NetworkUpgradeActive(nHeight, Consensus::UPGRADE_OVERWINTER); bool saplingActive = chainparams.GetConsensus().NetworkUpgradeActive(nHeight, Consensus::UPGRADE_SAPLING); bool isSprout = !overwinterActive; + bool heartwoodActive = chainparams.GetConsensus().NetworkUpgradeActive(nHeight, Consensus::UPGRADE_HEARTWOOD); // If Sprout rules apply, reject transactions which are intended for Overwinter and beyond if (isSprout && tx.fOverwintered) { @@ -887,6 +888,54 @@ bool ContextualCheckTransaction( REJECT_INVALID, "bad-txns-oversize"); } + // Rules that apply before Heartwood: + if (!heartwoodActive) { + if (tx.IsCoinBase()) { + // A coinbase transaction cannot have output descriptions + if (tx.vShieldedOutput.size() > 0) + return state.DoS( + dosLevelPotentiallyRelaxing, + error("CheckTransaction(): coinbase has output descriptions"), + REJECT_INVALID, "bad-cb-has-output-description"); + } + } + + // Rules that apply to Heartwood or later: + if (heartwoodActive) { + if (tx.IsCoinBase()) { + // All Sapling outputs in coinbase transactions MUST have valid note commitments + // when recovered using a 32-byte array of zeroes as the outgoing viewing key. + // https://zips.z.cash/zip-0213#specification + uint256 ovk; + for (const OutputDescription &output : tx.vShieldedOutput) { + auto outPlaintext = SaplingOutgoingPlaintext::decrypt( + output.outCiphertext, ovk, output.cv, output.cmu, output.ephemeralKey); + if (!outPlaintext) { + return state.DoS( + DOS_LEVEL_BLOCK, + error("CheckTransaction(): coinbase output description has invalid outCiphertext"), + REJECT_INVALID, + "bad-cb-output-desc-invalid-outct"); + } + + // SaplingNotePlaintext::decrypt() checks note commitment validity. + if (!SaplingNotePlaintext::decrypt( + output.encCiphertext, + output.ephemeralKey, + outPlaintext->esk, + outPlaintext->pk_d, + output.cmu) + ) { + return state.DoS( + DOS_LEVEL_BLOCK, + error("CheckTransaction(): coinbase output description has invalid encCiphertext"), + REJECT_INVALID, + "bad-cb-output-desc-invalid-encct"); + } + } + } + } + uint256 dataToBeSigned; if (!tx.vJoinSplit.empty() || @@ -1219,13 +1268,11 @@ bool CheckTransactionWithoutProofVerification(const CTransaction& tx, CValidatio return state.DoS(100, error("CheckTransaction(): coinbase has joinsplits"), REJECT_INVALID, "bad-cb-has-joinsplits"); - // A coinbase transaction cannot have spend descriptions or output descriptions + // A coinbase transaction cannot have spend descriptions if (tx.vShieldedSpend.size() > 0) return state.DoS(100, error("CheckTransaction(): coinbase has spend descriptions"), REJECT_INVALID, "bad-cb-has-spend-description"); - if (tx.vShieldedOutput.size() > 0) - return state.DoS(100, error("CheckTransaction(): coinbase has output descriptions"), - REJECT_INVALID, "bad-cb-has-output-description"); + // See ContextualCheckTransaction for consensus rules on coinbase output descriptions. if (tx.vin[0].scriptSig.size() < 2 || tx.vin[0].scriptSig.size() > 100) return state.DoS(100, error("CheckTransaction(): coinbase script size"), @@ -5305,8 +5352,12 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv, // Reject incoming connections from nodes that don't know about the current epoch const Consensus::Params& consensusParams = chainparams.GetConsensus(); auto currentEpoch = CurrentEpoch(GetHeight(), consensusParams); - if (pfrom->nVersion < consensusParams.vUpgrades[currentEpoch].nProtocolVersion) - { + if (pfrom->nVersion < consensusParams.vUpgrades[currentEpoch].nProtocolVersion && + !( + chainparams.NetworkIDString() == "regtest" && + !GetBoolArg("-nurejectoldversions", DEFAULT_NU_REJECT_OLD_VERSIONS) + ) + ) { LogPrintf("peer=%d using obsolete version %i; disconnecting\n", pfrom->id, pfrom->nVersion); pfrom->PushMessage("reject", strCommand, REJECT_OBSOLETE, strprintf("Version must be %d or greater", @@ -5438,8 +5489,12 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv, // 1. The version message has been received // 2. Peer version is below the minimum version for the current epoch else if (pfrom->nVersion < chainparams.GetConsensus().vUpgrades[ - CurrentEpoch(GetHeight(), chainparams.GetConsensus())].nProtocolVersion) - { + CurrentEpoch(GetHeight(), chainparams.GetConsensus())].nProtocolVersion && + !( + chainparams.NetworkIDString() == "regtest" && + !GetBoolArg("-nurejectoldversions", DEFAULT_NU_REJECT_OLD_VERSIONS) + ) + ) { LogPrintf("peer=%d using obsolete version %i; disconnecting\n", pfrom->id, pfrom->nVersion); pfrom->PushMessage("reject", strCommand, REJECT_OBSOLETE, strprintf("Version must be %d or greater", diff --git a/src/main.h b/src/main.h index 6bdb06d43..149175a8e 100644 --- a/src/main.h +++ b/src/main.h @@ -111,6 +111,9 @@ static const bool DEFAULT_CHECKPOINTS_ENABLED = true; static const bool DEFAULT_TXINDEX = false; static const unsigned int DEFAULT_BANSCORE_THRESHOLD = 100; +/** Default for -nurejectoldversions */ +static const bool DEFAULT_NU_REJECT_OLD_VERSIONS = true; + #define equihash_parameters_acceptable(N, K) \ ((CBlockHeader::HEADER_SIZE + equihash_solution_size(N, K))*MAX_HEADERS_RESULTS < \ MAX_PROTOCOL_MESSAGE_LENGTH-1000) diff --git a/src/miner.cpp b/src/miner.cpp index a8093542c..576d8d5b3 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -26,11 +26,13 @@ #include "primitives/transaction.h" #include "random.h" #include "timedata.h" +#include "transaction_builder.h" #include "ui_interface.h" #include "util.h" #include "utilmoneystr.h" #include "validationinterface.h" +#include #include "sodium.h" #include @@ -113,7 +115,94 @@ void UpdateTime(CBlockHeader* pblock, const Consensus::Params& consensusParams, } } -CBlockTemplate* CreateNewBlock(const CChainParams& chainparams, const CScript& scriptPubKeyIn) +bool IsValidMinerAddress(const MinerAddress& minerAddr) { + return minerAddr.which() != 0; +} + +class AddOutputsToCoinbaseTxAndSign : public boost::static_visitor<> +{ +private: + CMutableTransaction &mtx; + const CChainParams &chainparams; + const int nHeight; + const CAmount nFees; + +public: + AddOutputsToCoinbaseTxAndSign( + CMutableTransaction &mtx, + const CChainParams &chainparams, + const int nHeight, + const CAmount nFees) : mtx(mtx), chainparams(chainparams), nHeight(nHeight), nFees(nFees) {} + + CAmount SetFoundersRewardAndGetMinerValue() const { + auto value = GetBlockSubsidy(nHeight, chainparams.GetConsensus()); + + if ((nHeight > 0) && (nHeight <= chainparams.GetConsensus().GetLastFoundersRewardBlockHeight(nHeight))) { + // Founders reward is 20% of the block subsidy + auto vFoundersReward = value / 5; + // Take some reward away from us + value -= vFoundersReward; + // And give it to the founders + mtx.vout.push_back(CTxOut(vFoundersReward, chainparams.GetFoundersRewardScriptAtHeight(nHeight))); + } + + return value + nFees; + } + + void operator()(const InvalidMinerAddress &invalid) const {} + + // Create shielded output + void operator()(const libzcash::SaplingPaymentAddress &pa) const { + auto value = SetFoundersRewardAndGetMinerValue(); + mtx.valueBalance = -value; + + uint256 ovk; + auto note = libzcash::SaplingNote(pa, value); + auto output = OutputDescriptionInfo(ovk, note, {{0xF6}}); + + auto ctx = librustzcash_sapling_proving_ctx_init(); + + auto odesc = output.Build(ctx); + if (!odesc) { + librustzcash_sapling_proving_ctx_free(ctx); + throw new std::runtime_error("Failed to create shielded output for miner"); + } + mtx.vShieldedOutput.push_back(odesc.get()); + + // Empty output script. + uint256 dataToBeSigned; + CScript scriptCode; + try { + dataToBeSigned = SignatureHash( + scriptCode, mtx, NOT_AN_INPUT, SIGHASH_ALL, 0, + CurrentEpochBranchId(nHeight, chainparams.GetConsensus())); + } catch (std::logic_error ex) { + librustzcash_sapling_proving_ctx_free(ctx); + throw ex; + } + + librustzcash_sapling_binding_sig( + ctx, + mtx.valueBalance, + dataToBeSigned.begin(), + mtx.bindingSig.data()); + + librustzcash_sapling_proving_ctx_free(ctx); + } + + // Create transparent output + void operator()(const boost::shared_ptr &coinbaseScript) const { + // Create the miner's output. + mtx.vout.resize(1); + // Add the FR output and fetch the miner's output value. + auto value = SetFoundersRewardAndGetMinerValue(); + // Now fill in the miner's output. + mtx.vout[0].nValue = value; + mtx.vout[0].scriptPubKey = coinbaseScript->reserveScript; + } +}; + +CBlockTemplate* CreateNewBlock(const CChainParams& chainparams, const MinerAddress& minerAddress) { // Create new block std::unique_ptr pblocktemplate(new CBlockTemplate()); @@ -361,10 +450,6 @@ CBlockTemplate* CreateNewBlock(const CChainParams& chainparams, const CScript& s UpdateCoins(tx, view, nHeight); - BOOST_FOREACH(const OutputDescription &outDescription, tx.vShieldedOutput) { - sapling_tree.append(outDescription.cmu); - } - // Added pblock->vtx.push_back(tx); pblocktemplate->vTxFees.push_back(nTxFees); @@ -406,29 +491,26 @@ CBlockTemplate* CreateNewBlock(const CChainParams& chainparams, const CScript& s CMutableTransaction txNew = CreateNewContextualCMutableTransaction(chainparams.GetConsensus(), nHeight); txNew.vin.resize(1); txNew.vin[0].prevout.SetNull(); - txNew.vout.resize(1); - txNew.vout[0].scriptPubKey = scriptPubKeyIn; - txNew.vout[0].nValue = GetBlockSubsidy(nHeight, chainparams.GetConsensus()); // Set to 0 so expiry height does not apply to coinbase txs txNew.nExpiryHeight = 0; - if ((nHeight > 0) && (nHeight <= chainparams.GetConsensus().GetLastFoundersRewardBlockHeight(nHeight))) { - // Founders reward is 20% of the block subsidy - auto vFoundersReward = txNew.vout[0].nValue / 5; - // Take some reward away from us - txNew.vout[0].nValue -= vFoundersReward; + // Add outputs and sign + boost::apply_visitor( + AddOutputsToCoinbaseTxAndSign(txNew, chainparams, nHeight, nFees), + minerAddress); - // And give it to the founders - txNew.vout.push_back(CTxOut(vFoundersReward, chainparams.GetFoundersRewardScriptAtHeight(nHeight))); - } - - // Add fees - txNew.vout[0].nValue += nFees; txNew.vin[0].scriptSig = CScript() << nHeight << OP_0; pblock->vtx[0] = txNew; pblocktemplate->vTxFees[0] = -nFees; + // Update the Sapling commitment tree. + for (const CTransaction& tx : pblock->vtx) { + for (const OutputDescription& odesc : tx.vShieldedOutput) { + sapling_tree.append(odesc.cmu); + } + } + // Randomise nonce arith_uint256 nonce = UintToArith256(GetRandHash()); // Clear the top and bottom 16 bits (for local use as thread flags and counters) @@ -469,18 +551,26 @@ class MinerAddressScript : public CReserveScript void KeepScript() {} }; -void GetScriptForMinerAddress(boost::shared_ptr &script) +void GetMinerAddress(MinerAddress &minerAddress) { - CTxDestination addr = DecodeDestination(GetArg("-mineraddress", "")); - if (!IsValidDestination(addr)) { - return; + // Try a transparent address first + auto mAddrArg = GetArg("-mineraddress", ""); + CTxDestination addr = DecodeDestination(mAddrArg); + if (IsValidDestination(addr)) { + boost::shared_ptr mAddr(new MinerAddressScript()); + CKeyID keyID = boost::get(addr); + + mAddr->reserveScript = CScript() << OP_DUP << OP_HASH160 << ToByteVector(keyID) << OP_EQUALVERIFY << OP_CHECKSIG; + minerAddress = mAddr; + } else { + // Try a Sapling address + auto zaddr = DecodePaymentAddress(mAddrArg); + if (IsValidPaymentAddress(zaddr)) { + if (boost::get(&zaddr) != nullptr) { + minerAddress = boost::get(zaddr); + } + } } - - boost::shared_ptr mAddr(new MinerAddressScript()); - CKeyID keyID = boost::get(addr); - - script = mAddr; - script->reserveScript = CScript() << OP_DUP << OP_HASH160 << ToByteVector(keyID) << OP_EQUALVERIFY << OP_CHECKSIG; } void IncrementExtraNonce(CBlock* pblock, const CBlockIndex* pindexPrev, unsigned int& nExtraNonce) @@ -536,8 +626,8 @@ void static BitcoinMiner(const CChainParams& chainparams) // Each thread has its own counter unsigned int nExtraNonce = 0; - boost::shared_ptr coinbaseScript; - GetMainSignals().ScriptForMining(coinbaseScript); + MinerAddress minerAddress; + GetMainSignals().AddressForMining(minerAddress); unsigned int n = chainparams.GetConsensus().nEquihashN; unsigned int k = chainparams.GetConsensus().nEquihashK; @@ -557,9 +647,10 @@ void static BitcoinMiner(const CChainParams& chainparams) miningTimer.start(); try { - //throw an error if no script was provided - if (!coinbaseScript->reserveScript.size()) - throw std::runtime_error("No coinbase script available (mining requires a wallet or -mineraddress)"); + // Throw an error if no address valid for mining was provided. + if (!IsValidMinerAddress(minerAddress)) { + throw std::runtime_error("No miner address available (mining requires a wallet or -mineraddress)"); + } while (true) { if (chainparams.MiningRequiresPeers()) { @@ -585,7 +676,7 @@ void static BitcoinMiner(const CChainParams& chainparams) unsigned int nTransactionsUpdatedLast = mempool.GetTransactionsUpdated(); CBlockIndex* pindexPrev = chainActive.Tip(); - unique_ptr pblocktemplate(CreateNewBlock(chainparams, coinbaseScript->reserveScript)); + unique_ptr pblocktemplate(CreateNewBlock(chainparams, minerAddress)); if (!pblocktemplate.get()) { if (GetArg("-mineraddress", "").empty()) { @@ -633,7 +724,7 @@ void static BitcoinMiner(const CChainParams& chainparams) solver, pblock->nNonce.ToString()); std::function)> validBlock = - [&pblock, &hashTarget, &chainparams, &m_cs, &cancelSolver, &coinbaseScript] + [&pblock, &hashTarget, &chainparams, &m_cs, &cancelSolver, &minerAddress] (std::vector soln) { // Write the solution to the hash and compute the result. LogPrint("pow", "- Checking solution against target\n"); @@ -654,7 +745,7 @@ void static BitcoinMiner(const CChainParams& chainparams) cancelSolver = false; } SetThreadPriority(THREAD_PRIORITY_LOWEST); - coinbaseScript->KeepScript(); + boost::apply_visitor(KeepMinerAddress(), minerAddress); // In regression test mode, stop mining after a block is found. if (chainparams.MineBlocksOnDemand()) { diff --git a/src/miner.h b/src/miner.h index 1a6c286d6..0fa2dbb33 100644 --- a/src/miner.h +++ b/src/miner.h @@ -21,6 +21,28 @@ static const int DEFAULT_GENERATE_THREADS = 1; static const bool DEFAULT_PRINTPRIORITY = false; +class InvalidMinerAddress { +public: + friend bool operator==(const InvalidMinerAddress &a, const InvalidMinerAddress &b) { return true; } + friend bool operator<(const InvalidMinerAddress &a, const InvalidMinerAddress &b) { return true; } +}; + +typedef boost::variant> MinerAddress; + +class KeepMinerAddress : public boost::static_visitor<> +{ +public: + KeepMinerAddress() {} + + void operator()(const InvalidMinerAddress &invalid) const {} + void operator()(const libzcash::SaplingPaymentAddress &pa) const {} + void operator()(const boost::shared_ptr &coinbaseScript) const { + coinbaseScript->KeepScript(); + } +}; + +bool IsValidMinerAddress(const MinerAddress& minerAddr); + struct CBlockTemplate { CBlock block; @@ -29,11 +51,11 @@ struct CBlockTemplate }; /** Generate a new block, without valid proof-of-work */ -CBlockTemplate* CreateNewBlock(const CChainParams& chainparams, const CScript& scriptPubKeyIn); +CBlockTemplate* CreateNewBlock(const CChainParams& chainparams, const MinerAddress& minerAddress); #ifdef ENABLE_MINING -/** Get script for -mineraddress */ -void GetScriptForMinerAddress(boost::shared_ptr &script); +/** Get -mineraddress */ +void GetMinerAddress(MinerAddress &minerAddress); /** Modify the extranonce in a block */ void IncrementExtraNonce(CBlock* pblock, const CBlockIndex* pindexPrev, unsigned int& nExtraNonce); /** Run the miner threads */ diff --git a/src/net.cpp b/src/net.cpp index 18ab53fb8..318cc4059 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -832,7 +832,12 @@ static bool AttemptToEvictConnection(bool fPreferNewConnection) { { // Find any nodes which don't support the protocol version for the next upgrade for (const CNodeRef &node : vEvictionCandidates) { - if (node->nVersion < params.vUpgrades[idx].nProtocolVersion) { + if (node->nVersion < params.vUpgrades[idx].nProtocolVersion && + !( + Params().NetworkIDString() == "regtest" && + !GetBoolArg("-nurejectoldversions", DEFAULT_NU_REJECT_OLD_VERSIONS) + ) + ) { vTmpEvictionCandidates.push_back(node); } } diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index 727a30c82..81ac3872e 100644 --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -183,12 +183,13 @@ UniValue generate(const UniValue& params, bool fHelp) int nHeight = 0; int nGenerate = params[0].get_int(); - boost::shared_ptr coinbaseScript; - GetMainSignals().ScriptForMining(coinbaseScript); + MinerAddress minerAddress; + GetMainSignals().AddressForMining(minerAddress); - //throw an error if no script was provided - if (!coinbaseScript->reserveScript.size()) - throw JSONRPCError(RPC_INTERNAL_ERROR, "No coinbase script available (mining requires a wallet or -mineraddress)"); + // Throw an error if no address valid for mining was provided. + if (!IsValidMinerAddress(minerAddress)) { + throw JSONRPCError(RPC_INTERNAL_ERROR, "No miner address available (mining requires a wallet or -mineraddress)"); + } { // Don't keep cs_main locked LOCK(cs_main); @@ -202,7 +203,7 @@ UniValue generate(const UniValue& params, bool fHelp) unsigned int k = Params().GetConsensus().nEquihashK; while (nHeight < nHeightEnd) { - std::unique_ptr pblocktemplate(CreateNewBlock(Params(), coinbaseScript->reserveScript)); + std::unique_ptr pblocktemplate(CreateNewBlock(Params(), minerAddress)); if (!pblocktemplate.get()) throw JSONRPCError(RPC_INTERNAL_ERROR, "Couldn't create new block"); CBlock *pblock = &pblocktemplate->block; @@ -255,8 +256,8 @@ endloop: ++nHeight; blockHashes.push_back(pblock->GetHash().GetHex()); - //mark script as important because it was used at least for one coinbase output - coinbaseScript->KeepScript(); + //mark miner address as important because it was used at least for one coinbase output + boost::apply_visitor(KeepMinerAddress(), minerAddress); } return blockHashes; } @@ -610,19 +611,20 @@ UniValue getblocktemplate(const UniValue& params, bool fHelp) pblocktemplate = NULL; } - boost::shared_ptr coinbaseScript; - GetMainSignals().ScriptForMining(coinbaseScript); + MinerAddress minerAddress; + GetMainSignals().AddressForMining(minerAddress); - // Throw an error if no script was provided - if (!coinbaseScript->reserveScript.size()) - throw JSONRPCError(RPC_INTERNAL_ERROR, "No coinbase script available (mining requires a wallet or -mineraddress)"); + // Throw an error if no address valid for mining was provided. + if (!IsValidMinerAddress(minerAddress)) { + throw JSONRPCError(RPC_INTERNAL_ERROR, "No miner address available (mining requires a wallet or -mineraddress)"); + } - pblocktemplate = CreateNewBlock(Params(), coinbaseScript->reserveScript); + pblocktemplate = CreateNewBlock(Params(), minerAddress); if (!pblocktemplate) throw JSONRPCError(RPC_OUT_OF_MEMORY, "Out of memory"); // Mark script as important because it was used at least for one coinbase output - coinbaseScript->KeepScript(); + boost::apply_visitor(KeepMinerAddress(), minerAddress); // Need to update only after we know CreateNewBlock succeeded pindexPrev = pindexPrevNew; diff --git a/src/test/miner_tests.cpp b/src/test/miner_tests.cpp index c642a434f..1acbcad16 100644 --- a/src/test/miner_tests.cpp +++ b/src/test/miner_tests.cpp @@ -135,11 +135,23 @@ struct { {"00000000000000000000000000000000000000000000000000000000000022de", "007f388bed6b91756ea3e0866716ef6e9485fae6160195c7cda5c1e43f96ee359e105bcf4e8c293690420939124f04a0196363910421187811575929db40500b0bfdd1e8964aa334b801e3339a336d585a30852f1dc294a2d3d36f9ecc747458f3d41b4572415496df2a9fb1f882156cdabf9f65e681f38019865d6d47482277e24c9b8973eb34a41254faae4c5e2caa9dde5925ec118f3d8fa767ae00f434645957154367afe72000c59c79182c8faddd24424b9ebbb09ccd651b00540c96b9c7eec648a28a1d72c2e575d0f2250078511a011598db8e0788edf0ddc15ae24b62f63d6f93f71a2743a3c43ece55471a9802a76f31561a6f365c3647029bfa736395883afc0632bc25d4a8661b25d5aa0310f3c3fd3a183e75d359d6de3e5910b5dfbb74b7660af906917dc42b12e3e484aae1dbd20eaee037ef301572b7fc24d85b4aff9c82b27dcd421cee1639230d0188fec59f0dd4c0ced69c1ad07abd23692b1bd30735af942df597dcf6f403a36371bc416cf3e29a58570f586b05c357dc49515689788ad9581b8887dd913a41dc35e1ac9c9f9f4ea534eb6b36cc8af0299b6d3905750425da0366bdc59a7824477d7946b6f35c4ec90b8e61790fa74a4fa92396ea856661027828d40abb11dbe36bba516fe8ec8913106677285a4790d8034d1d1bf9fd87990889ddffc369b954a3d1c172be7e1812226c2b100cbe82c42bf4423456b6cb2bac3b4828135cb54f7a933a01f7f4a2057ad92136ba8e19fec313b412d43c089a71f06fd1625329b78d49ac92c59e4080932ddb1645910fd874dfb1f358e214231f62041acc41fd2c4e7b7127b3042459e1457f6b307fce9825aa4d2b942277f52665f2a77dd107b4f16cb3280f20c7551ff6cd855f97a6144131f69bab5648fb4b81261eefbf629094e8bcc4e36077f46d51a647da51fc01dca9a9ac12e2f7e2e2b1c9229dae099e95370177143d3b38ab661f19758494a01b32f0c27155b45a872a867dc50f9d76473695e9e2c4f9357f5ba6bb6c455d985f4e2c21486fde6576c6a8ceda6e010a7dc2b504130f429ac33376781ee4af5bbe8d768005bc4cb5092b15c4f296a8bd8c54a298eecd790a5161755a8605cc46bf890b8ff93d508501842b78c7261e5deeb1096891c528a300e57bf2f0aa9e8af2623cdf16bba20427704120484b6af8be26e4983d2685c783ce85d0174f84598719c6beefcc3603a94d4aa62750725df50671d7f9903ec255f779643ebd2fd8122fae3319e61928dcdaa44880d6a483140de63d2d7d7dc9dd449e0ee00d908e0f2164fc054198641e8fb0d74279c9b4117884b9335028a9f50c7223d3c03675ecf73329e52603f77f20cffba99356e51a365b75825f7db56d77542784f3c2663c493a2e564d73f753e9d6ebb0c2f2027a2330a7117c67a20507474fc47282a02cb572de17bbc7a335959316f74a05e3687cfb5227bc5b1b7f084f50902760e77740d420df9a495521c09b911e5f199a8343918b8386fc74f22552a76524a22c8c70ff06084e7fefe9b3ab98e004fadf35eb5f60483f287851712d90ebdd6b512877170d3b7fb34f16813917ae3b5ed54ede6081bdd7cc646fb336658121fd8fbafc52959b48d13375dfa4ce8616c157533a05ee1dc1120f215c348b54357d68adb4da7f5f48d55c005b3e7a23d05746e44d968f7601d4dbdff702861030d6e3a4140e6e1a29978be541f713f8e2cc9aa1ac32fecc941ee4aa4c41bc7f91ea5328ff87cbf35a8de17d1d3d1ad6d4384b0df52d10b3984d62e1678e86dd150ee425490bc727ee7107fda0f5d2433ab1c5d407be9d123fad5c201355601d926d3923787be86a4aa5be0b8d5750171ad658f8e97798b5dcaed46345a9af70c441"}, }; +// Copied from src/miner.cpp +class MinerAddressScript : public CReserveScript +{ + // CReserveScript requires implementing this function, so that if an + // internal (not-visible) wallet address is used, the wallet can mark it as + // important when a block is mined (so it then appears to the user). + // If -mineraddress is set, the user already knows about and is managing the + // address, so we don't need to do anything here. + void KeepScript() {} +}; + // NOTE: These tests rely on CreateNewBlock doing its own self-validation! BOOST_AUTO_TEST_CASE(CreateNewBlock_validity) { const CChainParams& chainparams = Params(CBaseChainParams::MAIN); - CScript scriptPubKey = CScript() << ParseHex("04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f") << OP_CHECKSIG; + boost::shared_ptr scriptPubKey(new MinerAddressScript()); + scriptPubKey->reserveScript = CScript() << ParseHex("04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f") << OP_CHECKSIG; CBlockTemplate *pblocktemplate; CMutableTransaction tx,tx2; CScript script; diff --git a/src/test/test_bitcoin.cpp b/src/test/test_bitcoin.cpp index 640472d56..5c86218b0 100644 --- a/src/test/test_bitcoin.cpp +++ b/src/test/test_bitcoin.cpp @@ -153,7 +153,10 @@ TestChain100Setup::CreateAndProcessBlock(const std::vector& unsigned int n = chainparams.GetConsensus().nEquihashN; unsigned int k = chainparams.GetConsensus().nEquihashK; - CBlockTemplate *pblocktemplate = CreateNewBlock(chainparams, scriptPubKey); + boost::shared_ptr mAddr(new CReserveScript()); + mAddr->reserveScript = scriptPubKey; + + CBlockTemplate *pblocktemplate = CreateNewBlock(chainparams, mAddr); CBlock& block = pblocktemplate->block; // Replace mempool-selected txns with just coinbase plus passed-in txns: diff --git a/src/test/transaction_tests.cpp b/src/test/transaction_tests.cpp index fadbc6c29..3660c709f 100644 --- a/src/test/transaction_tests.cpp +++ b/src/test/transaction_tests.cpp @@ -451,11 +451,6 @@ void test_simple_sapling_invalidity(uint32_t consensusBranchId, CMutableTransact vout.nValue = 1; newTx.vout.push_back(vout); - newTx.vShieldedOutput.push_back(OutputDescription()); - - BOOST_CHECK(!CheckTransactionWithoutProofVerification(newTx, state)); - BOOST_CHECK(state.GetRejectReason() == "bad-cb-has-output-description"); - newTx.vShieldedSpend.push_back(SpendDescription()); BOOST_CHECK(!CheckTransactionWithoutProofVerification(newTx, state)); diff --git a/src/transaction_builder.cpp b/src/transaction_builder.cpp index ae6c72cd3..6f917b314 100644 --- a/src/transaction_builder.cpp +++ b/src/transaction_builder.cpp @@ -22,6 +22,52 @@ SpendDescriptionInfo::SpendDescriptionInfo( librustzcash_sapling_generate_r(alpha.begin()); } +boost::optional OutputDescriptionInfo::Build(void* ctx) { + auto cmu = this->note.cmu(); + if (!cmu) { + return boost::none; + } + + libzcash::SaplingNotePlaintext notePlaintext(this->note, this->memo); + + auto res = notePlaintext.encrypt(this->note.pk_d); + if (!res) { + return boost::none; + } + auto enc = res.get(); + auto encryptor = enc.second; + + libzcash::SaplingPaymentAddress address(this->note.d, this->note.pk_d); + CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); + ss << address; + std::vector addressBytes(ss.begin(), ss.end()); + + OutputDescription odesc; + if (!librustzcash_sapling_output_proof( + ctx, + encryptor.get_esk().begin(), + addressBytes.data(), + this->note.r.begin(), + this->note.value(), + odesc.cv.begin(), + odesc.zkproof.begin())) { + return boost::none; + } + + odesc.cmu = *cmu; + odesc.ephemeralKey = encryptor.get_epk(); + odesc.encCiphertext = enc.first; + + libzcash::SaplingOutgoingPlaintext outPlaintext(this->note.pk_d, encryptor.get_esk()); + odesc.outCiphertext = outPlaintext.encrypt( + this->ovk, + odesc.cv, + odesc.cmu, + encryptor); + + return odesc; +} + TransactionBuilderResult::TransactionBuilderResult(const CTransaction& tx) : maybeTx(tx) {} TransactionBuilderResult::TransactionBuilderResult(const std::string& error) : maybeError(error) {} @@ -302,51 +348,19 @@ TransactionBuilderResult TransactionBuilder::Build() // Create Sapling OutputDescriptions for (auto output : outputs) { - auto cmu = output.note.cmu(); - if (!cmu) { + // Check this out here as well to provide better logging. + if (!output.note.cmu()) { librustzcash_sapling_proving_ctx_free(ctx); return TransactionBuilderResult("Output is invalid"); } - libzcash::SaplingNotePlaintext notePlaintext(output.note, output.memo); - - auto res = notePlaintext.encrypt(output.note.pk_d); - if (!res) { + auto odesc = output.Build(ctx); + if (!odesc) { librustzcash_sapling_proving_ctx_free(ctx); - return TransactionBuilderResult("Failed to encrypt note"); - } - auto enc = res.get(); - auto encryptor = enc.second; - - libzcash::SaplingPaymentAddress address(output.note.d, output.note.pk_d); - CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); - ss << address; - std::vector addressBytes(ss.begin(), ss.end()); - - OutputDescription odesc; - if (!librustzcash_sapling_output_proof( - ctx, - encryptor.get_esk().begin(), - addressBytes.data(), - output.note.r.begin(), - output.note.value(), - odesc.cv.begin(), - odesc.zkproof.begin())) { - librustzcash_sapling_proving_ctx_free(ctx); - return TransactionBuilderResult("Output proof failed"); + return TransactionBuilderResult("Failed to create output description"); } - odesc.cmu = *cmu; - odesc.ephemeralKey = encryptor.get_epk(); - odesc.encCiphertext = enc.first; - - libzcash::SaplingOutgoingPlaintext outPlaintext(output.note.pk_d, encryptor.get_esk()); - odesc.outCiphertext = outPlaintext.encrypt( - output.ovk, - odesc.cv, - odesc.cmu, - encryptor); - mtx.vShieldedOutput.push_back(odesc); + mtx.vShieldedOutput.push_back(odesc.get()); } // diff --git a/src/transaction_builder.h b/src/transaction_builder.h index 8db064c7f..d671e1089 100644 --- a/src/transaction_builder.h +++ b/src/transaction_builder.h @@ -43,6 +43,8 @@ struct OutputDescriptionInfo { uint256 ovk, libzcash::SaplingNote note, std::array memo) : ovk(ovk), note(note), memo(memo) {} + + boost::optional Build(void* ctx); }; struct TransparentInputInfo { diff --git a/src/utiltest.cpp b/src/utiltest.cpp index 965987349..c8e01fad5 100644 --- a/src/utiltest.cpp +++ b/src/utiltest.cpp @@ -230,6 +230,27 @@ void RegtestDeactivateBlossom() { SelectParams(CBaseChainParams::MAIN); } +const Consensus::Params& RegtestActivateHeartwood(bool updatePow, int heartwoodActivationHeight) { + SelectParams(CBaseChainParams::REGTEST); + UpdateNetworkUpgradeParameters(Consensus::UPGRADE_OVERWINTER, Consensus::NetworkUpgrade::ALWAYS_ACTIVE); + UpdateNetworkUpgradeParameters(Consensus::UPGRADE_SAPLING, Consensus::NetworkUpgrade::ALWAYS_ACTIVE); + UpdateNetworkUpgradeParameters(Consensus::UPGRADE_BLOSSOM, Consensus::NetworkUpgrade::ALWAYS_ACTIVE); + UpdateNetworkUpgradeParameters(Consensus::UPGRADE_HEARTWOOD, heartwoodActivationHeight); + if (updatePow) { + UpdateRegtestPow(32, 16, uint256S("0007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")); + } + return Params().GetConsensus(); +} + +void RegtestDeactivateHeartwood() { + UpdateRegtestPow(0, 0, uint256S("0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f")); + UpdateNetworkUpgradeParameters(Consensus::UPGRADE_HEARTWOOD, Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT); + UpdateNetworkUpgradeParameters(Consensus::UPGRADE_BLOSSOM, Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT); + UpdateNetworkUpgradeParameters(Consensus::UPGRADE_SAPLING, Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT); + UpdateNetworkUpgradeParameters(Consensus::UPGRADE_OVERWINTER, Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT); + SelectParams(CBaseChainParams::MAIN); +} + libzcash::SaplingExtendedSpendingKey GetTestMasterSaplingSpendingKey() { std::vector> rawSeed(32); diff --git a/src/utiltest.h b/src/utiltest.h index 141da8d4d..07409b786 100644 --- a/src/utiltest.h +++ b/src/utiltest.h @@ -49,6 +49,10 @@ const Consensus::Params& RegtestActivateBlossom(bool updatePow, int blossomActiv void RegtestDeactivateBlossom(); +const Consensus::Params& RegtestActivateHeartwood(bool updatePow, int heartwoodActivationHeight = Consensus::NetworkUpgrade::ALWAYS_ACTIVE); + +void RegtestDeactivateHeartwood(); + libzcash::SaplingExtendedSpendingKey GetTestMasterSaplingSpendingKey(); CKey AddTestCKeyToKeyStore(CBasicKeyStore& keyStore); diff --git a/src/validationinterface.cpp b/src/validationinterface.cpp index aa66ef7c7..49773aee8 100644 --- a/src/validationinterface.cpp +++ b/src/validationinterface.cpp @@ -33,13 +33,13 @@ void RegisterValidationInterface(CValidationInterface* pwalletIn) { g_signals.Inventory.connect(boost::bind(&CValidationInterface::Inventory, pwalletIn, _1)); g_signals.Broadcast.connect(boost::bind(&CValidationInterface::ResendWalletTransactions, pwalletIn, _1)); g_signals.BlockChecked.connect(boost::bind(&CValidationInterface::BlockChecked, pwalletIn, _1, _2)); - g_signals.ScriptForMining.connect(boost::bind(&CValidationInterface::GetScriptForMining, pwalletIn, _1)); + g_signals.AddressForMining.connect(boost::bind(&CValidationInterface::GetAddressForMining, pwalletIn, _1)); g_signals.BlockFound.connect(boost::bind(&CValidationInterface::ResetRequestCount, pwalletIn, _1)); } void UnregisterValidationInterface(CValidationInterface* pwalletIn) { g_signals.BlockFound.disconnect(boost::bind(&CValidationInterface::ResetRequestCount, pwalletIn, _1)); - g_signals.ScriptForMining.disconnect(boost::bind(&CValidationInterface::GetScriptForMining, pwalletIn, _1)); + g_signals.AddressForMining.disconnect(boost::bind(&CValidationInterface::GetAddressForMining, pwalletIn, _1)); g_signals.BlockChecked.disconnect(boost::bind(&CValidationInterface::BlockChecked, pwalletIn, _1, _2)); g_signals.Broadcast.disconnect(boost::bind(&CValidationInterface::ResendWalletTransactions, pwalletIn, _1)); g_signals.Inventory.disconnect(boost::bind(&CValidationInterface::Inventory, pwalletIn, _1)); @@ -53,7 +53,7 @@ void UnregisterValidationInterface(CValidationInterface* pwalletIn) { void UnregisterAllValidationInterfaces() { g_signals.BlockFound.disconnect_all_slots(); - g_signals.ScriptForMining.disconnect_all_slots(); + g_signals.AddressForMining.disconnect_all_slots(); g_signals.BlockChecked.disconnect_all_slots(); g_signals.Broadcast.disconnect_all_slots(); g_signals.Inventory.disconnect_all_slots(); diff --git a/src/validationinterface.h b/src/validationinterface.h index 3072e5a0a..8dfdd6eb4 100644 --- a/src/validationinterface.h +++ b/src/validationinterface.h @@ -9,6 +9,7 @@ #include #include +#include "miner.h" #include "zcash/IncrementalMerkleTree.hpp" class CBlock; @@ -40,7 +41,7 @@ protected: virtual void Inventory(const uint256 &hash) {} virtual void ResendWalletTransactions(int64_t nBestBlockTime) {} virtual void BlockChecked(const CBlock&, const CValidationState&) {} - virtual void GetScriptForMining(boost::shared_ptr&) {}; + virtual void GetAddressForMining(MinerAddress&) {}; virtual void ResetRequestCount(const uint256 &hash) {}; friend void ::RegisterValidationInterface(CValidationInterface*); friend void ::UnregisterValidationInterface(CValidationInterface*); @@ -66,8 +67,8 @@ struct CMainSignals { boost::signals2::signal Broadcast; /** Notifies listeners of a block validation result */ boost::signals2::signal BlockChecked; - /** Notifies listeners that a key for mining is required (coinbase) */ - boost::signals2::signal&)> ScriptForMining; + /** Notifies listeners that an address for mining is required (coinbase) */ + boost::signals2::signal AddressForMining; /** Notifies listeners that a block has been successfully mined */ boost::signals2::signal BlockFound; }; diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 22b79f8a2..c3d283a89 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -4245,7 +4245,7 @@ void CWallet::UpdatedTransaction(const uint256 &hashTx) } } -void CWallet::GetScriptForMining(boost::shared_ptr &script) +void CWallet::GetAddressForMining(MinerAddress &minerAddress) { if (!GetArg("-mineraddress", "").empty()) { return; @@ -4256,8 +4256,8 @@ void CWallet::GetScriptForMining(boost::shared_ptr &script) if (!rKey->GetReservedKey(pubkey)) return; - script = rKey; - script->reserveScript = CScript() << OP_DUP << OP_HASH160 << ToByteVector(pubkey.GetID()) << OP_EQUALVERIFY << OP_CHECKSIG; + rKey->reserveScript = CScript() << OP_DUP << OP_HASH160 << ToByteVector(pubkey.GetID()) << OP_EQUALVERIFY << OP_CHECKSIG; + minerAddress = rKey; } void CWallet::LockCoin(COutPoint& output) @@ -4885,12 +4885,16 @@ void CWallet::GetFilteredNotes( // Filter the transactions before checking for notes if (!CheckFinalTx(wtx) || - wtx.GetBlocksToMaturity() > 0 || wtx.GetDepthInMainChain() < minDepth || wtx.GetDepthInMainChain() > maxDepth) { continue; } + // Filter coinbase transactions that don't have Sapling outputs + if (wtx.IsCoinBase() && wtx.mapSaplingNoteData.empty()) { + continue; + } + for (auto & pair : wtx.mapSproutNoteData) { JSOutPoint jsop = pair.first; SproutNoteData nd = pair.second; diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index d4e36315b..40a326ae9 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -1268,7 +1268,7 @@ public: } } - void GetScriptForMining(boost::shared_ptr &script); + void GetAddressForMining(MinerAddress &minerAddress); void ResetRequestCount(const uint256 &hash) { LOCK(cs_wallet);