diff --git a/qa/rpc-tests/mining_shielded_coinbase.py b/qa/rpc-tests/mining_shielded_coinbase.py index 83c344011..9d9fc517f 100755 --- a/qa/rpc-tests/mining_shielded_coinbase.py +++ b/qa/rpc-tests/mining_shielded_coinbase.py @@ -6,10 +6,12 @@ from decimal import Decimal from test_framework.authproxy import JSONRPCException from test_framework.test_framework import BitcoinTestFramework -from test_framework.mininode import nuparams +from test_framework.mininode import COIN, nuparams from test_framework.util import ( BLOSSOM_BRANCH_ID, HEARTWOOD_BRANCH_ID, + CANOPY_BRANCH_ID, + NU5_BRANCH_ID, assert_equal, assert_raises, bitcoind_processes, @@ -28,8 +30,12 @@ class ShieldCoinbaseTest (BitcoinTestFramework): def start_node_with(self, index, extra_args=[]): args = [ + '-experimentalfeatures', + '-orchardwallet', nuparams(BLOSSOM_BRANCH_ID, 1), nuparams(HEARTWOOD_BRANCH_ID, 10), + nuparams(CANOPY_BRANCH_ID, 20), + nuparams(NU5_BRANCH_ID, 20), "-nurejectoldversions=false", ] return start_node(index, self.options.tmpdir, args + extra_args) @@ -117,5 +123,50 @@ class ShieldCoinbaseTest (BitcoinTestFramework): assert_equal(self.nodes[0].z_getbalance(node0_taddr), 2) assert_equal(self.nodes[1].z_getbalance(node1_zaddr), 1) + # Generate a Unified Address for node 1 + self.nodes[1].z_getnewaccount() + node1_addr0 = self.nodes[1].z_getaddressforaccount(0) + assert_equal(node1_addr0['account'], 0) + assert_equal(set(node1_addr0['pools']), set(['transparent', 'sapling', 'orchard'])) + node1_ua = node1_addr0['unifiedaddress'] + + # Set node 1's miner address to the UA + self.nodes[1].stop() + bitcoind_processes[1].wait() + self.nodes[1] = self.start_node_with(1, [ + "-mineraddress=%s" % node1_ua, + ]) + connect_nodes(self.nodes[1], 0) + + # The UA starts with zero balance. + assert_equal(self.nodes[1].z_getbalanceforaccount(0)['pools'], {}) + + # Node 1 can mine blocks because the miner selects the Sapling receiver + # of its UA. + print("Mining block with node 1") + self.nodes[1].generate(1) + self.sync_all() + + # The UA balance should show that Sapling funds were received. + assert_equal(self.nodes[1].z_getbalanceforaccount(0)['pools'], { + 'sapling': {'valueZat': 5 * COIN }, + }) + + # Activate NU5 + print("Activating NU5") + self.nodes[0].generate(7) + self.sync_all() + + # Now any block mined by node 1 should use the Orchard receiver of its UA. + print("Mining block with node 1") + self.nodes[1].generate(1) + self.sync_all() + assert_equal(self.nodes[1].z_getbalanceforaccount(0)['pools'], { + 'sapling': {'valueZat': 5 * COIN }, + # 6.25 ZEC because the FR always ends when Canopy activates, and + # regtest has no defined funding streams. + 'orchard': {'valueZat': 6.25 * COIN }, + }) + if __name__ == '__main__': ShieldCoinbaseTest().main() diff --git a/src/init.cpp b/src/init.cpp index e070bf944..b4f485bec 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -1105,7 +1105,7 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler) #ifdef ENABLE_MINING if (mapArgs.count("-mineraddress")) { auto addr = keyIO.DecodePaymentAddress(mapArgs["-mineraddress"]); - if (!(addr.has_value() && std::visit(ExtractMinerAddress(), addr.value()).has_value())) { + if (!(addr.has_value() && std::visit(ExtractMinerAddress(chainparams.GetConsensus(), 0), addr.value()).has_value())) { return InitError(strprintf( _("Invalid address for -mineraddress=: '%s' (must be a Sapling or transparent P2PKH address)"), mapArgs["-mineraddress"])); diff --git a/src/miner.cpp b/src/miner.cpp index 6e9f15fae..7d83400a7 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -214,31 +214,72 @@ public: return miner_reward + nFees; } - void ComputeBindingSig(void* ctx) const { + void ComputeBindingSig(void* saplingCtx, std::optional orchardBundle) const { // Empty output script. uint256 dataToBeSigned; - CScript scriptCode; try { - dataToBeSigned = SignatureHash( - scriptCode, mtx, NOT_AN_INPUT, SIGHASH_ALL, 0, - CurrentEpochBranchId(nHeight, chainparams.GetConsensus())); + if (orchardBundle.has_value()) { + // Orchard is only usable with v5+ transactions. + dataToBeSigned = ProduceZip244SignatureHash(mtx, orchardBundle.value()); + } else { + CScript scriptCode; + 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); + librustzcash_sapling_proving_ctx_free(saplingCtx); throw ex; } + if (orchardBundle.has_value()) { + auto authorizedBundle = orchardBundle.value().ProveAndSign(dataToBeSigned); + if (authorizedBundle.has_value()) { + mtx.orchardBundle = authorizedBundle.value(); + } else { + librustzcash_sapling_proving_ctx_free(saplingCtx); + throw new std::runtime_error("Failed to create Orchard proof or signatures"); + } + } + bool success = librustzcash_sapling_binding_sig( - ctx, + saplingCtx, mtx.valueBalanceSapling, dataToBeSigned.begin(), mtx.bindingSig.data()); if (!success) { - librustzcash_sapling_proving_ctx_free(ctx); + librustzcash_sapling_proving_ctx_free(saplingCtx); throw new std::runtime_error("An error occurred computing the binding signature."); } } + // Create Orchard output + void operator()(const libzcash::OrchardRawAddress &to) const { + auto ctx = librustzcash_sapling_proving_ctx_init(); + + // `enableSpends` must be set to `false` for coinbase transactions. This + // means the Orchard anchor is unconstrained, so we set it to the empty + // tree root via a null (all zeroes) uint256. + uint256 orchardAnchor; + auto builder = orchard::Builder(false, true, orchardAnchor); + + // Shielded coinbase outputs must be recoverable with an all-zeroes ovk. + uint256 ovk; + auto miner_reward = SetFoundersRewardAndGetMinerValue(ctx); + builder.AddOutput(ovk, to, miner_reward, std::nullopt); + + auto bundle = builder.Build(); + if (!bundle.has_value()) { + librustzcash_sapling_proving_ctx_free(ctx); + throw new std::runtime_error("Failed to create shielded output for miner"); + } + + ComputeBindingSig(ctx, std::move(bundle)); + + librustzcash_sapling_proving_ctx_free(ctx); + } + // Create shielded output void operator()(const libzcash::SaplingPaymentAddress &pa) const { auto ctx = librustzcash_sapling_proving_ctx_init(); @@ -258,7 +299,7 @@ public: } mtx.vShieldedOutput.push_back(odesc.value()); - ComputeBindingSig(ctx); + ComputeBindingSig(ctx, std::nullopt); librustzcash_sapling_proving_ctx_free(ctx); } @@ -277,7 +318,7 @@ public: mtx.vout[0] = CTxOut(value, coinbaseScript->reserveScript); if (mtx.vShieldedOutput.size() > 0) { - ComputeBindingSig(ctx); + ComputeBindingSig(ctx, std::nullopt); } librustzcash_sapling_proving_ctx_free(ctx); @@ -717,12 +758,19 @@ std::optional ExtractMinerAddress::operator()(const libzcash::Sapl return addr; } std::optional ExtractMinerAddress::operator()(const libzcash::UnifiedAddress &addr) const { - for (const auto& receiver: addr) { - if (std::holds_alternative(receiver)) { - return std::get(receiver); - } + auto preferred = addr.GetPreferredRecipientAddress(consensus, height); + if (preferred.has_value()) { + std::optional ret; + std::visit(match { + [&](const libzcash::OrchardRawAddress addr) { ret = MinerAddress(addr); }, + [&](const libzcash::SaplingPaymentAddress addr) { ret = MinerAddress(addr); }, + [&](const CKeyID keyID) { ret = operator()(keyID); }, + [&](const auto other) { ret = std::nullopt; } + }, preferred.value()); + return ret; + } else { + return std::nullopt; } - return std::nullopt; } @@ -730,10 +778,18 @@ void GetMinerAddress(std::optional &minerAddress) { KeyIO keyIO(Params()); + // If the user sets a UA miner address with an Orchard component, we want to ensure we + // start using it once we reach that height. + int height; + { + LOCK(cs_main); + height = chainActive.Height() + 1; + } + auto mAddrArg = GetArg("-mineraddress", ""); auto zaddr0 = keyIO.DecodePaymentAddress(mAddrArg); if (zaddr0.has_value()) { - auto zaddr = std::visit(ExtractMinerAddress(), zaddr0.value()); + auto zaddr = std::visit(ExtractMinerAddress(Params().GetConsensus(), height), zaddr0.value()); if (zaddr.has_value()) { minerAddress = zaddr.value(); } diff --git a/src/miner.h b/src/miner.h index 69a8f74ac..daa9689b6 100644 --- a/src/miner.h +++ b/src/miner.h @@ -24,13 +24,18 @@ static const int DEFAULT_GENERATE_THREADS = 1; static const bool DEFAULT_PRINTPRIORITY = false; typedef std::variant< + libzcash::OrchardRawAddress, libzcash::SaplingPaymentAddress, boost::shared_ptr> MinerAddress; class ExtractMinerAddress { + const Consensus::Params& consensus; + int height; + public: - ExtractMinerAddress() {} + ExtractMinerAddress(const Consensus::Params& consensus, int height) : + consensus(consensus), height(height) {} std::optional operator()(const CKeyID &keyID) const; std::optional operator()(const CScriptID &addr) const; @@ -44,6 +49,7 @@ class KeepMinerAddress public: KeepMinerAddress() {} + void operator()(const libzcash::OrchardRawAddress &addr) const {} void operator()(const libzcash::SaplingPaymentAddress &pa) const {} void operator()(const boost::shared_ptr &coinbaseScript) const { coinbaseScript->KeepScript(); @@ -57,6 +63,9 @@ class IsValidMinerAddress public: IsValidMinerAddress() {} + bool operator()(const libzcash::OrchardRawAddress &addr) const { + return true; + } bool operator()(const libzcash::SaplingPaymentAddress &pa) const { return true; }