From d95da694b90bf0599f8054b8a591b91c33617ded Mon Sep 17 00:00:00 2001 From: Duke Date: Fri, 8 Aug 2025 14:53:21 -0400 Subject: [PATCH 01/19] WIP donation via z_shieldcoinbase --- .../asyncrpcoperation_shieldcoinbase.cpp | 65 ++++++++++--------- src/wallet/asyncrpcoperation_shieldcoinbase.h | 9 +-- 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/src/wallet/asyncrpcoperation_shieldcoinbase.cpp b/src/wallet/asyncrpcoperation_shieldcoinbase.cpp index 57113f930..ce4da86fe 100644 --- a/src/wallet/asyncrpcoperation_shieldcoinbase.cpp +++ b/src/wallet/asyncrpcoperation_shieldcoinbase.cpp @@ -161,24 +161,7 @@ void AsyncRPCOperation_shieldcoinbase::main() { bool AsyncRPCOperation_shieldcoinbase::main_impl() { CAmount minersFee = fee_; - - size_t numInputs = inputs_.size(); - - /* - // Check mempooltxinputlimit to avoid creating a transaction which the local mempool rejects - size_t limit = (size_t)GetArg("-mempooltxinputlimit", 0); - { - LOCK(cs_main); - if (NetworkUpgradeActive(chainActive.Height() + 1, Params().GetConsensus(), Consensus::UPGRADE_OVERWINTER)) { - limit = 0; - } - } - if (limit>0 && numInputs > limit) { - throw JSONRPCError(RPC_WALLET_ERROR, - strprintf("Number of inputs %d is greater than mempooltxinputlimit of %d", - numInputs, limit)); - } - */ + size_t numInputs = inputs_.size(); CAmount targetAmount = 0; for (ShieldCoinbaseUTXO & utxo : inputs_) { @@ -192,10 +175,12 @@ bool AsyncRPCOperation_shieldcoinbase::main_impl() { } CAmount sendAmount = targetAmount - minersFee; - LogPrint("zrpc", "%s: spending %s to shield %s with fee %s\n", + LogPrint("zrpc", "%s: spending %s to shield %s with fee %s, donation=%d\n", getId(), FormatMoney(targetAmount), FormatMoney(sendAmount), FormatMoney(minersFee)); - return boost::apply_visitor(ShieldToAddress(this, sendAmount), tozaddr_); + //TODO: pass this in from RPC + CAmount donation = 2; + return boost::apply_visitor(ShieldToAddress(this, sendAmount, donation), tozaddr_); } extern UniValue signrawtransaction(const UniValue& params, bool fHelp, const CPubKey& mypk); @@ -229,19 +214,37 @@ bool ShieldToAddress::operator()(const libzcash::SaplingPaymentAddress &zaddr) c } } + //TODO: if donation==true, send X% of value to zaddr and Y% of of value to donatezaddr // Send all value to the target z-addr - m_op->builder_.SendChangeTo(zaddr, ovk); + if(donation) { + // add original recipient as first output + m_op->builder_.AddSaplingOutput(ovk, zaddr, sendAmount); + + CAmount donationAmount = 0; //TODO calculate exact donation amount in puposhis + // donation recipient as second output + m_op->builder_.AddSaplingOutput(ovk, donationZaddr, donationAmount); + + // zdust as third output, so donation txs are indistinguishable from + // non-donation z_shieldcoinbase txs + auto zdust1 = DecodePaymentAddress(randomSietchZaddr()); + auto sietchZout1 = boost::get(zdust1); + m_op->builder_.AddSaplingOutput(ovk, sietchZout1, 0); + + } else { + m_op->builder_.SendChangeTo(zaddr, ovk); + + // Sietchified Shielding of Coinbase Funds + // Add Sietch zouts so it's unclear which zout contains value :) + // This reduces metadata leakage of coinbase t=>z tx's + CAmount amount = 0; + auto zdust1 = DecodePaymentAddress(randomSietchZaddr()); + auto zdust2 = DecodePaymentAddress(randomSietchZaddr()); + auto sietchZout1 = boost::get(zdust1); + auto sietchZout2 = boost::get(zdust2); + m_op->builder_.AddSaplingOutput(ovk, sietchZout1, amount); + m_op->builder_.AddSaplingOutput(ovk, sietchZout2, amount); + } - // Sietchified Shielding of Coinbase Funds - // Add Sietch zouts so it's unclear which zout contains value :) - // This reduces metadata leakage of coinbase t=>z tx's - CAmount amount = 0; - auto zdust1 = DecodePaymentAddress(randomSietchZaddr()); - auto zdust2 = DecodePaymentAddress(randomSietchZaddr()); - auto sietchZout1 = boost::get(zdust1); - auto sietchZout2 = boost::get(zdust2); - m_op->builder_.AddSaplingOutput(ovk, sietchZout1, amount); - m_op->builder_.AddSaplingOutput(ovk, sietchZout2, amount); // Build the transaction auto maybe_tx = m_op->builder_.Build(); diff --git a/src/wallet/asyncrpcoperation_shieldcoinbase.h b/src/wallet/asyncrpcoperation_shieldcoinbase.h index bd606e23e..7a018e0d7 100644 --- a/src/wallet/asyncrpcoperation_shieldcoinbase.h +++ b/src/wallet/asyncrpcoperation_shieldcoinbase.h @@ -63,7 +63,6 @@ public: virtual UniValue getStatus() const; bool testmode = false; // Set to true to disable sending txs and generating proofs - bool cheatSpend = false; // set when this is shielding a cheating coinbase private: friend class ShieldToAddress; @@ -96,15 +95,15 @@ class ShieldToAddress : public boost::static_visitor private: AsyncRPCOperation_shieldcoinbase *m_op; CAmount sendAmount; + CAmount donation = 0; public: - ShieldToAddress(AsyncRPCOperation_shieldcoinbase *op, CAmount sendAmount) : - m_op(op), sendAmount(sendAmount) {} + ShieldToAddress(AsyncRPCOperation_shieldcoinbase *op, CAmount sendAmount, bool donation) : + m_op(op), sendAmount(sendAmount), donation(donation) {} bool operator()(const libzcash::SaplingPaymentAddress &zaddr) const; bool operator()(const libzcash::InvalidEncoding& no) const; }; - // To test private methods, a friend class can act as a proxy class TEST_FRIEND_AsyncRPCOperation_shieldcoinbase { public: @@ -135,6 +134,4 @@ public: } }; - #endif /* ASYNCRPCOPERATION_SHIELDCOINBASE_H */ - From 62da1981bb124062104de275980bf49f5a9412c6 Mon Sep 17 00:00:00 2001 From: Duke Date: Sun, 10 Aug 2025 13:46:07 -0400 Subject: [PATCH 02/19] WIP donation --- src/wallet/asyncrpcoperation_shieldcoinbase.h | 4 +-- src/wallet/rpcwallet.cpp | 30 ++++++++----------- 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/src/wallet/asyncrpcoperation_shieldcoinbase.h b/src/wallet/asyncrpcoperation_shieldcoinbase.h index 7a018e0d7..ce8de1c4d 100644 --- a/src/wallet/asyncrpcoperation_shieldcoinbase.h +++ b/src/wallet/asyncrpcoperation_shieldcoinbase.h @@ -49,6 +49,7 @@ public: std::vector inputs, std::string toAddress, CAmount fee = SHIELD_COINBASE_DEFAULT_MINERS_FEE, + CAmount donation, UniValue contextInfo = NullUniValue); virtual ~AsyncRPCOperation_shieldcoinbase(); @@ -73,9 +74,6 @@ private: CAmount fee_; PaymentAddress tozaddr_; - uint256 joinSplitPubKey_; - unsigned char joinSplitPrivKey_[crypto_sign_SECRETKEYBYTES]; - std::vector inputs_; TransactionBuilder builder_; diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index e86bcdd59..89bbe75b6 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -5290,18 +5290,6 @@ UniValue z_sendmany(const UniValue& params, bool fHelp, const CPubKey& mypk) mtx.nVersionGroupId = SAPLING_VERSION_GROUP_ID; mtx.nVersion = SAPLING_TX_VERSION; unsigned int max_tx_size = MAX_TX_SIZE_AFTER_SAPLING; - /* - const bool sapling = true; //NetworkUpgradeActive(nextBlockHeight, Params().GetConsensus(), Consensus::UPGRADE_SAPLING)) { - if (!sapling) { - if (NetworkUpgradeActive(nextBlockHeight, Params().GetConsensus(), Consensus::UPGRADE_OVERWINTER)) { - mtx.nVersionGroupId = OVERWINTER_VERSION_GROUP_ID; - mtx.nVersion = OVERWINTER_TX_VERSION; - } else { - mtx.fOverwintered = false; - mtx.nVersion = 2; - } - } - */ // As a sanity check, estimate and verify that the size of the transaction will be valid. // Depending on the input notes, the actual tx size may turn out to be larger and perhaps invalid. @@ -5420,7 +5408,7 @@ UniValue z_shieldcoinbase(const UniValue& params, bool fHelp, const CPubKey& myp if (!EnsureWalletIsAvailable(fHelp)) return NullUniValue; - if (fHelp || params.size() < 2 || params.size() > 4) + if (fHelp || params.size() < 2 || params.size() > 5) throw runtime_error( "z_shieldcoinbase \"fromaddress\" \"tozaddress\" ( fee ) ( limit )\n" "\nShield transparent coinbase funds by sending to a shielded zaddr. This is an asynchronous operation and utxos" @@ -5492,6 +5480,14 @@ UniValue z_shieldcoinbase(const UniValue& params, bool fHelp, const CPubKey& myp } } + uint8_t donation = 0; + if (params.size() > 4) { + donation = params[4].get_int(); + if (donation < 1 || donation > 10 ) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid donation percentage, must be an integer between 1 and 10"); + } + } + int nextBlockHeight = chainActive.Height() + 1; bool overwinterActive = nextBlockHeight>=1 ? true : false; // NetworkUpgradeActive(nextBlockHeight, Params().GetConsensus(), Consensus::UPGRADE_OVERWINTER); unsigned int max_tx_size = MAX_TX_SIZE_AFTER_SAPLING; @@ -5592,15 +5588,13 @@ UniValue z_shieldcoinbase(const UniValue& params, bool fHelp, const CPubKey& myp contextInfo.push_back(Pair("fromaddress", params[0])); contextInfo.push_back(Pair("toaddress", params[1])); contextInfo.push_back(Pair("fee", ValueFromAmount(nFee))); + contextInfo.push_back(Pair("donation", donation)); - // Builder (used if Sapling addresses are involved) - TransactionBuilder builder = TransactionBuilder( - Params().GetConsensus(), nextBlockHeight, pwalletMain); + TransactionBuilder builder = TransactionBuilder( Params().GetConsensus(), nextBlockHeight, pwalletMain); // Contextual transaction we will build on int blockHeight = chainActive.LastTip()->GetHeight(); nextBlockHeight = blockHeight + 1; - // (used if no Sapling addresses are involved) CMutableTransaction contextualTx = CreateNewContextualCMutableTransaction( Params().GetConsensus(), nextBlockHeight); contextualTx.nLockTime = chainActive.LastTip()->GetHeight(); @@ -5611,7 +5605,7 @@ UniValue z_shieldcoinbase(const UniValue& params, bool fHelp, const CPubKey& myp // Create operation and add to global queue std::shared_ptr q = getAsyncRPCQueue(); - std::shared_ptr operation( new AsyncRPCOperation_shieldcoinbase(builder, contextualTx, inputs, destaddress, nFee, contextInfo) ); + std::shared_ptr operation( new AsyncRPCOperation_shieldcoinbase(builder, contextualTx, inputs, destaddress, nFee, donation, contextInfo) ); q->addOperation(operation); AsyncRPCOperationId operationId = operation->getId(); From 02a26751bb51cf6a576790c487872a21b5beccb5 Mon Sep 17 00:00:00 2001 From: Duke Date: Mon, 13 Oct 2025 15:06:42 -0400 Subject: [PATCH 03/19] WIP donation --- src/wallet/asyncrpcoperation_shieldcoinbase.cpp | 11 ++++++++++- src/wallet/asyncrpcoperation_shieldcoinbase.h | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/wallet/asyncrpcoperation_shieldcoinbase.cpp b/src/wallet/asyncrpcoperation_shieldcoinbase.cpp index ce4da86fe..dfb1932a1 100644 --- a/src/wallet/asyncrpcoperation_shieldcoinbase.cpp +++ b/src/wallet/asyncrpcoperation_shieldcoinbase.cpp @@ -54,6 +54,7 @@ AsyncRPCOperation_shieldcoinbase::AsyncRPCOperation_shieldcoinbase( std::vector inputs, std::string toAddress, CAmount fee, + CAmount donation, UniValue contextInfo) : builder_(builder), tx_(contextualTx), inputs_(inputs), fee_(fee), contextinfo_(contextInfo) { @@ -214,6 +215,13 @@ bool ShieldToAddress::operator()(const libzcash::SaplingPaymentAddress &zaddr) c } } + //TODO: randomly select from a set + auto dzaddr = "zs1..."; + auto donationZaddr = DecodePaymentAddress(dzaddr); + if (!IsValidPaymentAddress(donationZaddr)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, string("Unknown address format: ") + dzaddr); + } + //TODO: if donation==true, send X% of value to zaddr and Y% of of value to donatezaddr // Send all value to the target z-addr if(donation) { @@ -222,7 +230,8 @@ bool ShieldToAddress::operator()(const libzcash::SaplingPaymentAddress &zaddr) c CAmount donationAmount = 0; //TODO calculate exact donation amount in puposhis // donation recipient as second output - m_op->builder_.AddSaplingOutput(ovk, donationZaddr, donationAmount); + auto donationZout = boost::get(donationZaddr); + m_op->builder_.AddSaplingOutput(ovk, donationZout, donationAmount); // zdust as third output, so donation txs are indistinguishable from // non-donation z_shieldcoinbase txs diff --git a/src/wallet/asyncrpcoperation_shieldcoinbase.h b/src/wallet/asyncrpcoperation_shieldcoinbase.h index ce8de1c4d..c07b638fb 100644 --- a/src/wallet/asyncrpcoperation_shieldcoinbase.h +++ b/src/wallet/asyncrpcoperation_shieldcoinbase.h @@ -49,7 +49,7 @@ public: std::vector inputs, std::string toAddress, CAmount fee = SHIELD_COINBASE_DEFAULT_MINERS_FEE, - CAmount donation, + CAmount donation = 0, UniValue contextInfo = NullUniValue); virtual ~AsyncRPCOperation_shieldcoinbase(); From 1f50e635a0671f73c4085bb6d5b8b99e93695d73 Mon Sep 17 00:00:00 2001 From: Duke Date: Mon, 13 Oct 2025 15:27:30 -0400 Subject: [PATCH 04/19] WIP donation --- src/wallet/asyncrpcoperation_shieldcoinbase.cpp | 17 ++++++++++------- src/wallet/asyncrpcoperation_shieldcoinbase.h | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/wallet/asyncrpcoperation_shieldcoinbase.cpp b/src/wallet/asyncrpcoperation_shieldcoinbase.cpp index dfb1932a1..7a39d90bf 100644 --- a/src/wallet/asyncrpcoperation_shieldcoinbase.cpp +++ b/src/wallet/asyncrpcoperation_shieldcoinbase.cpp @@ -219,19 +219,21 @@ bool ShieldToAddress::operator()(const libzcash::SaplingPaymentAddress &zaddr) c auto dzaddr = "zs1..."; auto donationZaddr = DecodePaymentAddress(dzaddr); if (!IsValidPaymentAddress(donationZaddr)) { - throw JSONRPCError(RPC_INVALID_PARAMETER, string("Unknown address format: ") + dzaddr); + throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid donation zaddr, Unknown address format: ") + dzaddr); } - //TODO: if donation==true, send X% of value to zaddr and Y% of of value to donatezaddr - // Send all value to the target z-addr + //TODO: if donation>0, send X% of value to zaddr and Y% of of value to donatezaddr + // where X+Y=100% if(donation) { - // add original recipient as first output - m_op->builder_.AddSaplingOutput(ovk, zaddr, sendAmount); + //TODO: calculate exact values of sendAmount and donationAmount + CAmount donationAmount = (donation/100)*sendAmount; + // add original recipient as first output, with sendAmount less the donation + m_op->builder_.AddSaplingOutput(ovk, zaddr, sendAmount - donationAmount); - CAmount donationAmount = 0; //TODO calculate exact donation amount in puposhis - // donation recipient as second output auto donationZout = boost::get(donationZaddr); m_op->builder_.AddSaplingOutput(ovk, donationZout, donationAmount); + + fprintf(stderr,"%s: donation=%ld, sendAmount=%ld, donationAmount=%ld\n", __func__, donation, sendAmount, donationAmount); // zdust as third output, so donation txs are indistinguishable from // non-donation z_shieldcoinbase txs @@ -240,6 +242,7 @@ bool ShieldToAddress::operator()(const libzcash::SaplingPaymentAddress &zaddr) c m_op->builder_.AddSaplingOutput(ovk, sietchZout1, 0); } else { + // Send all value to the target z-addr m_op->builder_.SendChangeTo(zaddr, ovk); // Sietchified Shielding of Coinbase Funds diff --git a/src/wallet/asyncrpcoperation_shieldcoinbase.h b/src/wallet/asyncrpcoperation_shieldcoinbase.h index 36d3c5e6e..312c9111f 100644 --- a/src/wallet/asyncrpcoperation_shieldcoinbase.h +++ b/src/wallet/asyncrpcoperation_shieldcoinbase.h @@ -49,7 +49,7 @@ public: std::vector inputs, std::string toAddress, CAmount fee = SHIELD_COINBASE_DEFAULT_MINERS_FEE, - CAmount donation = 0, + CAmount donation = 0, //TODO: uint8_t UniValue contextInfo = NullUniValue); virtual ~AsyncRPCOperation_shieldcoinbase(); From 23ef00cfd7cd468f8aee5644ca6ce7dea977b16a Mon Sep 17 00:00:00 2001 From: Duke Date: Mon, 13 Oct 2025 18:21:51 -0400 Subject: [PATCH 05/19] WIP donation test --- qa/rpc-tests/shieldcoinbase_donation.py | 144 ++++++++++++++++++ .../asyncrpcoperation_shieldcoinbase.cpp | 6 +- test.sh | 3 +- 3 files changed, 149 insertions(+), 4 deletions(-) create mode 100755 qa/rpc-tests/shieldcoinbase_donation.py diff --git a/qa/rpc-tests/shieldcoinbase_donation.py b/qa/rpc-tests/shieldcoinbase_donation.py new file mode 100755 index 000000000..34686058a --- /dev/null +++ b/qa/rpc-tests/shieldcoinbase_donation.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +# Copyright (c) 2016-2025 The Hush developers +# Distributed under the GPLv3 software license, see the accompanying +# file COPYING or https://www.gnu.org/licenses/gpl-3.0.en.html + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.authproxy import JSONRPCException +from test_framework.util import assert_equal, assert_greater_than, \ + initialize_chain_clean, initialize_chain, start_nodes, start_node, connect_nodes_bi, \ + stop_nodes, sync_blocks, sync_mempools, wait_bitcoinds, rpc_port, assert_raises, assert_true, \ + wait_and_assert_operationid_status + +import time +from decimal import Decimal + +def assert_success(result): + assert_equal(result['result'], 'success') + +def assert_error(result): + assert_equal(result['result'], 'error') + +class LockZinsTest (BitcoinTestFramework): + def setup_chain(self): + print("Initializing test directory "+self.options.tmpdir) + self.num_nodes = 2 + self.options.nocleanup = 1 # do not delete datadir after test run + #self.options.nocleanup = 0 + initialize_chain_clean(self.options.tmpdir, self.num_nodes) + + def setup_network(self, split = False): + print("Setting up network...") + self.supply = 555 + self.nodes = start_nodes(self.num_nodes, self.options.tmpdir, + extra_args=[[ + # always give -ac_name as first extra_arg and port as third + '-ac_name=ZZZ', + #'-ac_algo=randomx', + #'-testnode=1', # why does this make the test node hang before run_test() ? + '-conf='+self.options.tmpdir+'/node0/regtest/ZZZ.conf', + '-port=63367', + '-rpcport=63368', + '-ac_supply=' + str(self.supply), + '-ac_reward=100000000', + '-ac_private=1', + '-allowlist=127.0.0.1', + '-regtest', + '--daemon', + #'-debug', + #'-zrpc', + #'-zdebug', + ##'-zrpcunsafe' + ],[ + '-ac_name=ZZZ', + '-conf='+self.options.tmpdir+'/node1/regtest/ZZZ.conf', + '-port=63357', + '-rpcport=63358', + '-ac_supply=' + str(self.supply), + '-ac_reward=100000000', + '-ac_private=1', + '-allowlist=127.0.0.1', + '-regtest', + '--daemon', + #'-debug', + #'-zrpc', + #'-zdebug', + ##'-zrpcunsafe' + ]] + ) + self.is_network_split = False + self.rpc = self.nodes[0] + self.sync_all() + print("Done setting up network") + + def run_test (self): + print("Mining blocks...") + rpc0 = self.nodes[0] + rpc1 = self.nodes[1] + + # mine initial ac_supply + rpc0.generate(1) + rpc1.generate(1) + self.sync_all() + + zaddr1 = rpc0.z_getnewaddress() + #rpc0.z_exportkey(zaddr1) + + # first we test the default situation where no donation is given and + # it defaults to 0 + response = rpc0.z_shieldcoinbase('*', zaddr1, 0, 1) + opid = response['opid'] + shieldingValue = response['shieldingValue'] + + assert_true( shieldingValue >= self.supply ) + + print("opid=" + opid) + + time.sleep(2) # give some time for the ztx to complete + + # 555 supply plus magic utxo = 555.02070592 + totalSupply = 55502070592 # in puposhis + expectedAmount = totalSupply + + json = rpc0.z_getoperationstatus() + txid = json[0]['result']['txid'] + + wait_and_assert_operationid_status(rpc0, opid) + + rawtx0 = rpc0.z_viewtransaction(txid) + assert_equal( rawtx0['outputs'][0]['valueZat'] , expectedAmount, '5% donation sends correct sendAmount') + + # now we test giving a donation parameter + donation = 5 + response = rpc1.z_shieldcoinbase('*', zaddr1, 0, 1, donation) + opid = response['opid'] + shieldingValue = response['shieldingValue'] + + assert_true( shieldingValue >= self.supply ) + + rpc0.generate(1) + rpc1.generate(1) + self.sync_all() + + print("opid=" + opid) + + time.sleep(2) # give some time for the ztx to complete + + # 555 supply plus magic utxo = 555.02070592 + totalSupply = 55502070592 + expectedAmount = 52726967062 # 95% of above supply, rounded to closest satoshi + + json = rpc1.z_getoperationstatus() + txid = json[0]['result']['txid'] + + wait_and_assert_operationid_status(rpc1, opid) + + #TODO: sync with testing zaddr in src/wallet/asyncrpcoperation_shieldcoinbase.cpp + rpc1.z_importkey(testing_privkey) + + rawtx1 = rpc1.z_viewtransaction(txid) + assert_equal( rawtx1['outputs'][0]['valueZat'] , totalSupply - expectedAmount, '5% donation sends correct donationAmount') + + +if __name__ == '__main__': + LockZinsTest ().main() diff --git a/src/wallet/asyncrpcoperation_shieldcoinbase.cpp b/src/wallet/asyncrpcoperation_shieldcoinbase.cpp index 7a39d90bf..adb8c748e 100644 --- a/src/wallet/asyncrpcoperation_shieldcoinbase.cpp +++ b/src/wallet/asyncrpcoperation_shieldcoinbase.cpp @@ -214,9 +214,9 @@ bool ShieldToAddress::operator()(const libzcash::SaplingPaymentAddress &zaddr) c m_op->builder_.AddTransparentInput(COutPoint(t.txid, t.vout), t.scriptPubKey, t.amount); } } - + //TODO: TESTING zaddr only, only use on regtest //TODO: randomly select from a set - auto dzaddr = "zs1..."; + auto dzaddr = "zregtestsapling1y30nwg0clsu6gcyrnvht8hdyfk3vwtszlh6kc4z5hv9hmpxzg2g0nx7c60xeecggm9x9gma96t4"; auto donationZaddr = DecodePaymentAddress(dzaddr); if (!IsValidPaymentAddress(donationZaddr)) { throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid donation zaddr, Unknown address format: ") + dzaddr); @@ -233,7 +233,7 @@ bool ShieldToAddress::operator()(const libzcash::SaplingPaymentAddress &zaddr) c auto donationZout = boost::get(donationZaddr); m_op->builder_.AddSaplingOutput(ovk, donationZout, donationAmount); - fprintf(stderr,"%s: donation=%ld, sendAmount=%ld, donationAmount=%ld\n", __func__, donation, sendAmount, donationAmount); + LogPrintf("%s: donation=%ld, sendAmount=%ld, donationAmount=%ld\n", __func__, donation, sendAmount, donationAmount); // zdust as third output, so donation txs are indistinguishable from // non-donation z_shieldcoinbase txs diff --git a/test.sh b/test.sh index a48d5f3f8..dba7a8c15 100755 --- a/test.sh +++ b/test.sh @@ -7,4 +7,5 @@ export PYTHONPATH=./qa/rpc-tests/test_framework/ #./qa/rpc-tests/ac_private.py -./qa/rpc-tests/lockzins.py --tracerpc +# ./qa/rpc-tests/lockzins.py --tracerpc + ./qa/rpc-tests/shieldcoinbase_donation.py --tracerpc From 61bae5fb7b618522afdbfe724601a04b7da9d7f2 Mon Sep 17 00:00:00 2001 From: Duke Date: Tue, 14 Oct 2025 03:49:06 -0400 Subject: [PATCH 06/19] WIP donation test --- qa/rpc-tests/shieldcoinbase_donation.py | 63 +++++++++++++++++-------- 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/qa/rpc-tests/shieldcoinbase_donation.py b/qa/rpc-tests/shieldcoinbase_donation.py index 34686058a..c755c14ee 100755 --- a/qa/rpc-tests/shieldcoinbase_donation.py +++ b/qa/rpc-tests/shieldcoinbase_donation.py @@ -5,7 +5,7 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.authproxy import JSONRPCException -from test_framework.util import assert_equal, assert_greater_than, \ +from test_framework.util import assert_equal, assert_greater_than, assert_greater_than_or_equal, \ initialize_chain_clean, initialize_chain, start_nodes, start_node, connect_nodes_bi, \ stop_nodes, sync_blocks, sync_mempools, wait_bitcoinds, rpc_port, assert_raises, assert_true, \ wait_and_assert_operationid_status @@ -19,7 +19,7 @@ def assert_success(result): def assert_error(result): assert_equal(result['result'], 'error') -class LockZinsTest (BitcoinTestFramework): +class ShieldCoinbaseDonationTest (BitcoinTestFramework): def setup_chain(self): print("Initializing test directory "+self.options.tmpdir) self.num_nodes = 2 @@ -72,7 +72,10 @@ class LockZinsTest (BitcoinTestFramework): print("Done setting up network") def run_test (self): - print("Mining blocks...") + self.run_test_default() + self.run_test_custom() + + def run_test_default(self): rpc0 = self.nodes[0] rpc1 = self.nodes[1] @@ -108,37 +111,59 @@ class LockZinsTest (BitcoinTestFramework): rawtx0 = rpc0.z_viewtransaction(txid) assert_equal( rawtx0['outputs'][0]['valueZat'] , expectedAmount, '5% donation sends correct sendAmount') + def run_test_custom(self): + rpc0 = self.nodes[0] + rpc1 = self.nodes[1] + zaddr1 = rpc0.z_getnewaddress() + + # generate some new coinbase funds + rpc0.generate(1) + rpc1.generate(1) + self.sync_all() + + # zregtestsapling1y30nwg0clsu6gcyrnvht8hdyfk3vwtszlh6kc4z5hv9hmpxzg2g0nx7c60xeecggm9x9gma96t4 + testing_privkey = "secret-extended-key-regtest1q0hgrms7qqqqpqrsz6myrtnh3ccp8uzp0kgxj6029wr6vq5hqvyccdlz7a745pgm5eeaamxqp9rxll2xctfrlw2l8xhxsc7zsut2tyz0374rrlk8srjswx7rhm6hcf2d7fuwajazvjesafduzxyka4w02tqjxdehzvghyrsd2zll90k3g2ckdvc5kqd6r7r7nglrtj0ej5a40d6lh8zxrvdlxrpuc59y5m8n9tekdxh4wpqn3smv5nxu4vvu58f8dgwn92qfqrvxqlscchtyh" + + # import zaddr that receives donation to node1, no rescan + rpc1.z_importkey(testing_privkey, "no") + + rpc1.z_listaddresses() + # now we test giving a donation parameter donation = 5 - response = rpc1.z_shieldcoinbase('*', zaddr1, 0, 1, donation) + + # shield funds to a new zaddr in this node0 wallet, donation goes to + # node1 wallet + response = rpc0.z_shieldcoinbase('*', zaddr1, 0, 1, donation) opid = response['opid'] + print("opid=" + opid) + #wait_and_assert_operationid_status(rpc0, opid) shieldingValue = response['shieldingValue'] - assert_true( shieldingValue >= self.supply ) + assert_greater_than_or_equal( shieldingValue , 1.0 ) + + time.sleep(2) # give some time for the ztx to complete + + rpc0.getinfo() + rpc1.getinfo() rpc0.generate(1) rpc1.generate(1) self.sync_all() - print("opid=" + opid) - - time.sleep(2) # give some time for the ztx to complete - - # 555 supply plus magic utxo = 555.02070592 - totalSupply = 55502070592 - expectedAmount = 52726967062 # 95% of above supply, rounded to closest satoshi - - json = rpc1.z_getoperationstatus() + # get the txid from node0 that created it + json = rpc0.z_getoperationstatus() txid = json[0]['result']['txid'] + print("txid=" + txid) - wait_and_assert_operationid_status(rpc1, opid) + rpc1.z_listunspent() - #TODO: sync with testing zaddr in src/wallet/asyncrpcoperation_shieldcoinbase.cpp - rpc1.z_importkey(testing_privkey) + expectedAmount = 5000000 + # lookup txid on node1 which should have received donation rawtx1 = rpc1.z_viewtransaction(txid) - assert_equal( rawtx1['outputs'][0]['valueZat'] , totalSupply - expectedAmount, '5% donation sends correct donationAmount') + assert_equal( rawtx1['outputs'][0]['valueZat'] , expectedAmount, '5% donation sends correct donationAmount') if __name__ == '__main__': - LockZinsTest ().main() + ShieldCoinbaseDonationTest().main() From c3b9b0914415ea31bdad6a7ad033153bb8902905 Mon Sep 17 00:00:00 2001 From: Duke Date: Tue, 14 Oct 2025 03:58:00 -0400 Subject: [PATCH 07/19] Make rpc error correct for all chains --- src/wallet/rpcwallet.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 3521247cc..0272ca089 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -4828,7 +4828,7 @@ UniValue z_viewtransaction(const UniValue& params, bool fHelp, const CPubKey& my UniValue entry(UniValue::VOBJ); if (!pwalletMain->mapWallet.count(hash)) - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid or non-wallet Hush transaction id!"); + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid or non-wallet transaction id!"); const CWalletTx& wtx = pwalletMain->mapWallet[hash]; entry.push_back(Pair("txid", hash.GetHex())); From 75564c82aca568e68164a2a773e48c7db1b35fbd Mon Sep 17 00:00:00 2001 From: Duke Date: Tue, 14 Oct 2025 04:16:15 -0400 Subject: [PATCH 08/19] Failing test for shielded donation --- qa/rpc-tests/shieldcoinbase_donation.py | 52 ++++++++++--------------- 1 file changed, 20 insertions(+), 32 deletions(-) diff --git a/qa/rpc-tests/shieldcoinbase_donation.py b/qa/rpc-tests/shieldcoinbase_donation.py index c755c14ee..dc77ef0c1 100755 --- a/qa/rpc-tests/shieldcoinbase_donation.py +++ b/qa/rpc-tests/shieldcoinbase_donation.py @@ -22,7 +22,7 @@ def assert_error(result): class ShieldCoinbaseDonationTest (BitcoinTestFramework): def setup_chain(self): print("Initializing test directory "+self.options.tmpdir) - self.num_nodes = 2 + self.num_nodes = 1 self.options.nocleanup = 1 # do not delete datadir after test run #self.options.nocleanup = 0 initialize_chain_clean(self.options.tmpdir, self.num_nodes) @@ -49,21 +49,6 @@ class ShieldCoinbaseDonationTest (BitcoinTestFramework): #'-zrpc', #'-zdebug', ##'-zrpcunsafe' - ],[ - '-ac_name=ZZZ', - '-conf='+self.options.tmpdir+'/node1/regtest/ZZZ.conf', - '-port=63357', - '-rpcport=63358', - '-ac_supply=' + str(self.supply), - '-ac_reward=100000000', - '-ac_private=1', - '-allowlist=127.0.0.1', - '-regtest', - '--daemon', - #'-debug', - #'-zrpc', - #'-zdebug', - ##'-zrpcunsafe' ]] ) self.is_network_split = False @@ -77,11 +62,9 @@ class ShieldCoinbaseDonationTest (BitcoinTestFramework): def run_test_default(self): rpc0 = self.nodes[0] - rpc1 = self.nodes[1] # mine initial ac_supply rpc0.generate(1) - rpc1.generate(1) self.sync_all() zaddr1 = rpc0.z_getnewaddress() @@ -113,27 +96,24 @@ class ShieldCoinbaseDonationTest (BitcoinTestFramework): def run_test_custom(self): rpc0 = self.nodes[0] - rpc1 = self.nodes[1] zaddr1 = rpc0.z_getnewaddress() # generate some new coinbase funds rpc0.generate(1) - rpc1.generate(1) self.sync_all() - # zregtestsapling1y30nwg0clsu6gcyrnvht8hdyfk3vwtszlh6kc4z5hv9hmpxzg2g0nx7c60xeecggm9x9gma96t4 + testing_zaddr = "zregtestsapling1y30nwg0clsu6gcyrnvht8hdyfk3vwtszlh6kc4z5hv9hmpxzg2g0nx7c60xeecggm9x9gma96t4" testing_privkey = "secret-extended-key-regtest1q0hgrms7qqqqpqrsz6myrtnh3ccp8uzp0kgxj6029wr6vq5hqvyccdlz7a745pgm5eeaamxqp9rxll2xctfrlw2l8xhxsc7zsut2tyz0374rrlk8srjswx7rhm6hcf2d7fuwajazvjesafduzxyka4w02tqjxdehzvghyrsd2zll90k3g2ckdvc5kqd6r7r7nglrtj0ej5a40d6lh8zxrvdlxrpuc59y5m8n9tekdxh4wpqn3smv5nxu4vvu58f8dgwn92qfqrvxqlscchtyh" - # import zaddr that receives donation to node1, no rescan - rpc1.z_importkey(testing_privkey, "no") + # import zaddr that receives donation , no rescan + rpc0.z_importkey(testing_privkey, "no") - rpc1.z_listaddresses() + rpc0.z_listaddresses() # now we test giving a donation parameter donation = 5 - # shield funds to a new zaddr in this node0 wallet, donation goes to - # node1 wallet + # shield funds to a new zaddr in this wallet response = rpc0.z_shieldcoinbase('*', zaddr1, 0, 1, donation) opid = response['opid'] print("opid=" + opid) @@ -145,25 +125,33 @@ class ShieldCoinbaseDonationTest (BitcoinTestFramework): time.sleep(2) # give some time for the ztx to complete rpc0.getinfo() - rpc1.getinfo() rpc0.generate(1) - rpc1.generate(1) self.sync_all() - # get the txid from node0 that created it + # get the txid json = rpc0.z_getoperationstatus() txid = json[0]['result']['txid'] print("txid=" + txid) - rpc1.z_listunspent() + rpc0.z_listunspent() expectedAmount = 5000000 # lookup txid on node1 which should have received donation - rawtx1 = rpc1.z_viewtransaction(txid) - assert_equal( rawtx1['outputs'][0]['valueZat'] , expectedAmount, '5% donation sends correct donationAmount') + rawtx1 = rpc0.z_viewtransaction(txid) + # there should be two outputs to different addresses, order is nondeterministic + if rawtx1['outputs'][0]['address'] == testing_zaddr: + zout = 0 + else: + zout = 1 + + assert_equal( rawtx1['outputs'][zout]['address'] , testing_zaddr, 'correct zaddr gets donation') + assert_equal( rawtx1['outputs'][zout]['valueZat'] , expectedAmount, '5% donation sends correct donationAmount') if __name__ == '__main__': ShieldCoinbaseDonationTest().main() + + + From caf7178ffd550351bb8cc169bc363644f2c4a762 Mon Sep 17 00:00:00 2001 From: Duke Date: Tue, 14 Oct 2025 10:57:40 -0400 Subject: [PATCH 09/19] Allow donation=0 --- qa/rpc-tests/shieldcoinbase_donation.py | 2 -- src/wallet/rpcwallet.cpp | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/qa/rpc-tests/shieldcoinbase_donation.py b/qa/rpc-tests/shieldcoinbase_donation.py index dc77ef0c1..e50c1ea3c 100755 --- a/qa/rpc-tests/shieldcoinbase_donation.py +++ b/qa/rpc-tests/shieldcoinbase_donation.py @@ -153,5 +153,3 @@ class ShieldCoinbaseDonationTest (BitcoinTestFramework): if __name__ == '__main__': ShieldCoinbaseDonationTest().main() - - diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 0272ca089..1896044e4 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -5562,7 +5562,7 @@ UniValue z_shieldcoinbase(const UniValue& params, bool fHelp, const CPubKey& myp uint8_t donation = 0; if (params.size() > 4) { donation = params[4].get_int(); - if (donation < 1 || donation > 10 ) { + if (donation < 0 || donation > 10 ) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid donation percentage, must be an integer between 1 and 10"); } } From 606b28d6ca5655b8d896f19a9fb3d6a9c2d2de16 Mon Sep 17 00:00:00 2001 From: Duke Date: Tue, 14 Oct 2025 11:00:08 -0400 Subject: [PATCH 10/19] Improve rpc errors and docs --- src/wallet/rpcwallet.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 1896044e4..734cdd15c 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -1341,19 +1341,19 @@ UniValue sendfrom(const UniValue& params, bool fHelp, const CPubKey& mypk) "\nSend 0.01 " + strprintf("%s",hush_chainname()) + " from the default account to the address, must have at least 1 confirmation\n" + HelpExampleCli("sendfrom", "\"\" \"RD6GgnrMpPaTSMn8vai6yiGA7mN4QGPV\" 0.01") + "\nSend 0.01 from the tabby account to the given address, funds must have at least 6 confirmations\n" - + HelpExampleCli("sendfrom", "\"tabby\" \"RD6GgnrMpPaTSMn8vai6yiGA7mN4QGPV\" 0.01 6 \"donation\" \"seans outpost\"") + + + HelpExampleCli("sendfrom", "\"tabby\" \"RD6GgnrMpPaTSMn8vai6yiGA7mN4QGPV\" 0.01 6 \"donation\" \"Hush Puppy Freedom Fund\"") + "\nAs a json rpc call\n" - + HelpExampleRpc("sendfrom", "\"tabby\", \"RD6GgnrMpPaTSMn8vai6yiGA7mN4QGPV\", 0.01, 6, \"donation\", \"seans outpost\"") + + HelpExampleRpc("sendfrom", "\"tabby\", \"RD6GgnrMpPaTSMn8vai6yiGA7mN4QGPV\", 0.01, 6, \"donation\", \"Hush Puppy Freedom Fund\"") ); if ( ASSETCHAINS_PRIVATE != 0 ) - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "cant use transparent addresses in private chain"); + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Cannot use transparent addresses in private chain"); LOCK2(cs_main, pwalletMain->cs_wallet); std::string strAccount = AccountFromValue(params[0]); CTxDestination dest = DecodeDestination(params[1].get_str()); if (!IsValidDestination(dest)) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Hush address!"); + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid address!"); } CAmount nAmount = AmountFromValue(params[2]); if (nAmount <= 0) From ebde772adad957eb24dd3f2a0548961f41f85d9d Mon Sep 17 00:00:00 2001 From: Duke Date: Tue, 14 Oct 2025 11:52:05 -0400 Subject: [PATCH 11/19] WIP donation test --- qa/rpc-tests/shieldcoinbase_donation.py | 6 +++--- .../asyncrpcoperation_shieldcoinbase.cpp | 19 +++++++++++-------- src/wallet/asyncrpcoperation_shieldcoinbase.h | 7 +++++-- src/wallet/rpcwallet.cpp | 5 ++++- 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/qa/rpc-tests/shieldcoinbase_donation.py b/qa/rpc-tests/shieldcoinbase_donation.py index e50c1ea3c..010ed3548 100755 --- a/qa/rpc-tests/shieldcoinbase_donation.py +++ b/qa/rpc-tests/shieldcoinbase_donation.py @@ -46,9 +46,9 @@ class ShieldCoinbaseDonationTest (BitcoinTestFramework): '-regtest', '--daemon', #'-debug', - #'-zrpc', - #'-zdebug', - ##'-zrpcunsafe' + '-zrpc', + '-zdebug', + '-zrpcunsafe' ]] ) self.is_network_split = False diff --git a/src/wallet/asyncrpcoperation_shieldcoinbase.cpp b/src/wallet/asyncrpcoperation_shieldcoinbase.cpp index adb8c748e..a11908230 100644 --- a/src/wallet/asyncrpcoperation_shieldcoinbase.cpp +++ b/src/wallet/asyncrpcoperation_shieldcoinbase.cpp @@ -54,9 +54,9 @@ AsyncRPCOperation_shieldcoinbase::AsyncRPCOperation_shieldcoinbase( std::vector inputs, std::string toAddress, CAmount fee, - CAmount donation, + uint8_t donation, UniValue contextInfo) : - builder_(builder), tx_(contextualTx), inputs_(inputs), fee_(fee), contextinfo_(contextInfo) + builder_(builder), tx_(contextualTx), inputs_(inputs), fee_(fee), donation_(donation), contextinfo_(contextInfo) { assert(contextualTx.nVersion >= 2); // transaction format version must support vjoinsplit @@ -68,6 +68,10 @@ AsyncRPCOperation_shieldcoinbase::AsyncRPCOperation_shieldcoinbase( throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, "Empty inputs"); } + if (donation < 0 || donation > 10 ) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid donation percentage, must be an integer between 0 and 10"); + } + // Check the destination address is valid for this network i.e. not testnet being used on mainnet auto address = DecodePaymentAddress(toAddress); if (IsValidPaymentAddress(address)) { @@ -177,11 +181,9 @@ bool AsyncRPCOperation_shieldcoinbase::main_impl() { CAmount sendAmount = targetAmount - minersFee; LogPrint("zrpc", "%s: spending %s to shield %s with fee %s, donation=%d\n", - getId(), FormatMoney(targetAmount), FormatMoney(sendAmount), FormatMoney(minersFee)); + getId(), FormatMoney(targetAmount), FormatMoney(sendAmount), FormatMoney(minersFee), donation_); - //TODO: pass this in from RPC - CAmount donation = 2; - return boost::apply_visitor(ShieldToAddress(this, sendAmount, donation), tozaddr_); + return boost::apply_visitor(ShieldToAddress(this, sendAmount, donation_), tozaddr_); } extern UniValue signrawtransaction(const UniValue& params, bool fHelp, const CPubKey& mypk); @@ -225,8 +227,9 @@ bool ShieldToAddress::operator()(const libzcash::SaplingPaymentAddress &zaddr) c //TODO: if donation>0, send X% of value to zaddr and Y% of of value to donatezaddr // where X+Y=100% if(donation) { - //TODO: calculate exact values of sendAmount and donationAmount - CAmount donationAmount = (donation/100)*sendAmount; + // calculate donation as a double then convert to CAmount + CAmount donationAmount = (CAmount) ( ((double)donation/100)*sendAmount ); + // add original recipient as first output, with sendAmount less the donation m_op->builder_.AddSaplingOutput(ovk, zaddr, sendAmount - donationAmount); diff --git a/src/wallet/asyncrpcoperation_shieldcoinbase.h b/src/wallet/asyncrpcoperation_shieldcoinbase.h index 312c9111f..8ef85bdc0 100644 --- a/src/wallet/asyncrpcoperation_shieldcoinbase.h +++ b/src/wallet/asyncrpcoperation_shieldcoinbase.h @@ -49,7 +49,7 @@ public: std::vector inputs, std::string toAddress, CAmount fee = SHIELD_COINBASE_DEFAULT_MINERS_FEE, - CAmount donation = 0, //TODO: uint8_t + uint8_t donation = 0, UniValue contextInfo = NullUniValue); virtual ~AsyncRPCOperation_shieldcoinbase(); @@ -76,6 +76,9 @@ private: std::vector inputs_; + // this is a donation % between 0 and 10, not a CAmount + uint8_t donation_; + TransactionBuilder builder_; CTransaction tx_; @@ -92,7 +95,7 @@ class ShieldToAddress : public boost::static_visitor private: AsyncRPCOperation_shieldcoinbase *m_op; CAmount sendAmount; - CAmount donation = 0; + uint8_t donation = 0; public: ShieldToAddress(AsyncRPCOperation_shieldcoinbase *op, CAmount sendAmount, bool donation) : m_op(op), sendAmount(sendAmount), donation(donation) {} diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 734cdd15c..7d816491b 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -5563,10 +5563,12 @@ UniValue z_shieldcoinbase(const UniValue& params, bool fHelp, const CPubKey& myp if (params.size() > 4) { donation = params[4].get_int(); if (donation < 0 || donation > 10 ) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid donation percentage, must be an integer between 1 and 10"); + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid donation percentage, must be an integer between 0 and 10"); } } + LogPrintf("%s: donation=%d\n", __func__, donation); + int nextBlockHeight = chainActive.Height() + 1; bool overwinterActive = nextBlockHeight>=1 ? true : false; // NetworkUpgradeActive(nextBlockHeight, Params().GetConsensus(), Consensus::UPGRADE_OVERWINTER); unsigned int max_tx_size = MAX_TX_SIZE_AFTER_SAPLING; @@ -5694,6 +5696,7 @@ UniValue z_shieldcoinbase(const UniValue& params, bool fHelp, const CPubKey& myp o.push_back(Pair("remainingValue", ValueFromAmount(remainingValue))); o.push_back(Pair("shieldingUTXOs", static_cast(numUtxos))); o.push_back(Pair("shieldingValue", ValueFromAmount(shieldedValue))); + o.push_back(Pair("donation", donation)); o.push_back(Pair("opid", operationId)); return o; } From 42a676d277ba21f91254467556abd110be4e4b16 Mon Sep 17 00:00:00 2001 From: Duke Date: Tue, 14 Oct 2025 12:20:35 -0400 Subject: [PATCH 12/19] Make the shieldcoinbase donation test pass --- src/wallet/asyncrpcoperation_shieldcoinbase.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wallet/asyncrpcoperation_shieldcoinbase.h b/src/wallet/asyncrpcoperation_shieldcoinbase.h index 8ef85bdc0..121d214b9 100644 --- a/src/wallet/asyncrpcoperation_shieldcoinbase.h +++ b/src/wallet/asyncrpcoperation_shieldcoinbase.h @@ -97,7 +97,7 @@ private: CAmount sendAmount; uint8_t donation = 0; public: - ShieldToAddress(AsyncRPCOperation_shieldcoinbase *op, CAmount sendAmount, bool donation) : + ShieldToAddress(AsyncRPCOperation_shieldcoinbase *op, CAmount sendAmount, uint8_t donation) : m_op(op), sendAmount(sendAmount), donation(donation) {} bool operator()(const libzcash::SaplingPaymentAddress &zaddr) const; From 69dadd112861107a3c4711f6c7e2e530b67853a4 Mon Sep 17 00:00:00 2001 From: Duke Date: Tue, 14 Oct 2025 13:19:18 -0400 Subject: [PATCH 13/19] Better tests; use current drgx block reward and default fee --- qa/rpc-tests/shieldcoinbase_donation.py | 36 ++++++++++++++++++------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/qa/rpc-tests/shieldcoinbase_donation.py b/qa/rpc-tests/shieldcoinbase_donation.py index 010ed3548..53ae1356c 100755 --- a/qa/rpc-tests/shieldcoinbase_donation.py +++ b/qa/rpc-tests/shieldcoinbase_donation.py @@ -40,7 +40,7 @@ class ShieldCoinbaseDonationTest (BitcoinTestFramework): '-port=63367', '-rpcport=63368', '-ac_supply=' + str(self.supply), - '-ac_reward=100000000', + '-ac_reward=300000000', '-ac_private=1', '-allowlist=127.0.0.1', '-regtest', @@ -82,8 +82,10 @@ class ShieldCoinbaseDonationTest (BitcoinTestFramework): time.sleep(2) # give some time for the ztx to complete - # 555 supply plus magic utxo = 555.02070592 - totalSupply = 55502070592 # in puposhis + # 555 supply plus magic utxo = 555.05537304 + # NOTE: if any consensus params for this testcoin are changed above, + # the magic utxo will change and this value will need to be updated + totalSupply = 55505537304 # in puposhis expectedAmount = totalSupply json = rpc0.z_getoperationstatus() @@ -113,8 +115,9 @@ class ShieldCoinbaseDonationTest (BitcoinTestFramework): # now we test giving a donation parameter donation = 5 - # shield funds to a new zaddr in this wallet - response = rpc0.z_shieldcoinbase('*', zaddr1, 0, 1, donation) + # shield funds to a new zaddr in this wallet with default fee + fee = 0.0001 + response = rpc0.z_shieldcoinbase('*', zaddr1, fee, 1, donation) opid = response['opid'] print("opid=" + opid) #wait_and_assert_operationid_status(rpc0, opid) @@ -136,18 +139,31 @@ class ShieldCoinbaseDonationTest (BitcoinTestFramework): rpc0.z_listunspent() - expectedAmount = 5000000 + # (3 - fee)*0.05 + expectedAmount1 = 14999500 # lookup txid on node1 which should have received donation rawtx1 = rpc0.z_viewtransaction(txid) # there should be two outputs to different addresses, order is nondeterministic if rawtx1['outputs'][0]['address'] == testing_zaddr: - zout = 0 + donation_zout = 0 + other_zout = 1 else: - zout = 1 + donation_zout = 1 + other_zout = 0 - assert_equal( rawtx1['outputs'][zout]['address'] , testing_zaddr, 'correct zaddr gets donation') - assert_equal( rawtx1['outputs'][zout]['valueZat'] , expectedAmount, '5% donation sends correct donationAmount') + # print("donation_zout=%d other_zout=%d" % (donation_zout,other_zout) ) + + assert_equal( rawtx1['outputs'][donation_zout]['address'] , testing_zaddr, 'correct zaddr gets donation') + assert_equal( rawtx1['outputs'][donation_zout]['valueZat'] , expectedAmount1, '5% donation sends correct donationAmount') + + # (3 - fee)*0.95 + expectedAmount2 = 284990500 + assert_equal( rawtx1['outputs'][other_zout]['address'] , zaddr1, 'correct zaddr gets main amount') + assert_equal( rawtx1['outputs'][other_zout]['valueZat'] , expectedAmount2, '5% donation sends correct sendAmount') + + + assert_equal( expectedAmount1 + expectedAmount2, 299990000, 'sendAmount+donationAmount = targetAmount - fee' ) if __name__ == '__main__': From fc7aa68203daa85329a9b76b11ce345bca7bce36 Mon Sep 17 00:00:00 2001 From: Duke Date: Tue, 14 Oct 2025 14:24:37 -0400 Subject: [PATCH 14/19] Refactor donation test now that we are using just one node --- qa/rpc-tests/shieldcoinbase_donation.py | 47 ++++++++++++++----------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/qa/rpc-tests/shieldcoinbase_donation.py b/qa/rpc-tests/shieldcoinbase_donation.py index 53ae1356c..672caf51d 100755 --- a/qa/rpc-tests/shieldcoinbase_donation.py +++ b/qa/rpc-tests/shieldcoinbase_donation.py @@ -57,22 +57,24 @@ class ShieldCoinbaseDonationTest (BitcoinTestFramework): print("Done setting up network") def run_test (self): + # NOTE: order of these tests is important self.run_test_default() self.run_test_custom() + self.run_test_custom_nondefault_fee() def run_test_default(self): - rpc0 = self.nodes[0] + rpc = self.nodes[0] # mine initial ac_supply - rpc0.generate(1) + rpc.generate(1) self.sync_all() - zaddr1 = rpc0.z_getnewaddress() - #rpc0.z_exportkey(zaddr1) + zaddr1 = rpc.z_getnewaddress() + #rpc.z_exportkey(zaddr1) # first we test the default situation where no donation is given and # it defaults to 0 - response = rpc0.z_shieldcoinbase('*', zaddr1, 0, 1) + response = rpc.z_shieldcoinbase('*', zaddr1, 0, 1) opid = response['opid'] shieldingValue = response['shieldingValue'] @@ -88,62 +90,67 @@ class ShieldCoinbaseDonationTest (BitcoinTestFramework): totalSupply = 55505537304 # in puposhis expectedAmount = totalSupply - json = rpc0.z_getoperationstatus() + json = rpc.z_getoperationstatus() txid = json[0]['result']['txid'] - wait_and_assert_operationid_status(rpc0, opid) + wait_and_assert_operationid_status(rpc, opid) - rawtx0 = rpc0.z_viewtransaction(txid) + rawtx0 = rpc.z_viewtransaction(txid) assert_equal( rawtx0['outputs'][0]['valueZat'] , expectedAmount, '5% donation sends correct sendAmount') + def run_test_custom_nondefault_fee(self): + rpc = self.nodes[0] + + # donation zaddr is already imported from previous test + def run_test_custom(self): - rpc0 = self.nodes[0] - zaddr1 = rpc0.z_getnewaddress() + rpc = self.nodes[0] + zaddr1 = rpc.z_getnewaddress() # generate some new coinbase funds - rpc0.generate(1) + rpc.generate(1) self.sync_all() testing_zaddr = "zregtestsapling1y30nwg0clsu6gcyrnvht8hdyfk3vwtszlh6kc4z5hv9hmpxzg2g0nx7c60xeecggm9x9gma96t4" testing_privkey = "secret-extended-key-regtest1q0hgrms7qqqqpqrsz6myrtnh3ccp8uzp0kgxj6029wr6vq5hqvyccdlz7a745pgm5eeaamxqp9rxll2xctfrlw2l8xhxsc7zsut2tyz0374rrlk8srjswx7rhm6hcf2d7fuwajazvjesafduzxyka4w02tqjxdehzvghyrsd2zll90k3g2ckdvc5kqd6r7r7nglrtj0ej5a40d6lh8zxrvdlxrpuc59y5m8n9tekdxh4wpqn3smv5nxu4vvu58f8dgwn92qfqrvxqlscchtyh" # import zaddr that receives donation , no rescan - rpc0.z_importkey(testing_privkey, "no") + rpc.z_importkey(testing_privkey, "no") - rpc0.z_listaddresses() + rpc.z_listaddresses() # now we test giving a donation parameter donation = 5 # shield funds to a new zaddr in this wallet with default fee fee = 0.0001 - response = rpc0.z_shieldcoinbase('*', zaddr1, fee, 1, donation) + response = rpc.z_shieldcoinbase('*', zaddr1, fee, 1, donation) opid = response['opid'] print("opid=" + opid) - #wait_and_assert_operationid_status(rpc0, opid) + #wait_and_assert_operationid_status(rpc, opid) shieldingValue = response['shieldingValue'] assert_greater_than_or_equal( shieldingValue , 1.0 ) time.sleep(2) # give some time for the ztx to complete - rpc0.getinfo() + rpc.getinfo() - rpc0.generate(1) + rpc.generate(1) self.sync_all() # get the txid - json = rpc0.z_getoperationstatus() + json = rpc.z_getoperationstatus() txid = json[0]['result']['txid'] print("txid=" + txid) - rpc0.z_listunspent() + rpc.z_listunspent() # (3 - fee)*0.05 expectedAmount1 = 14999500 # lookup txid on node1 which should have received donation - rawtx1 = rpc0.z_viewtransaction(txid) + rawtx1 = rpc.z_viewtransaction(txid) # there should be two outputs to different addresses, order is nondeterministic if rawtx1['outputs'][0]['address'] == testing_zaddr: donation_zout = 0 From e73251ad9261061f8b275e5ba5a783f1f7b85314 Mon Sep 17 00:00:00 2001 From: Duke Date: Tue, 14 Oct 2025 22:29:10 -0400 Subject: [PATCH 15/19] WIP donation test --- qa/rpc-tests/shieldcoinbase_donation.py | 75 ++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/qa/rpc-tests/shieldcoinbase_donation.py b/qa/rpc-tests/shieldcoinbase_donation.py index 672caf51d..42e6b7329 100755 --- a/qa/rpc-tests/shieldcoinbase_donation.py +++ b/qa/rpc-tests/shieldcoinbase_donation.py @@ -100,8 +100,81 @@ class ShieldCoinbaseDonationTest (BitcoinTestFramework): def run_test_custom_nondefault_fee(self): rpc = self.nodes[0] + zaddr1 = rpc.z_getnewaddress() + + donation = 5 # donation zaddr is already imported from previous test + # NOTE: goal here is to test a situation where + # sendAmount/donationAmount arithmetic leads to a situation where the + # exact amount in satoshis must deal with truncation/rounding + + # generate some new coinbase funds + rpc.generate(1) + self.sync_all() + + # shield funds to a new zaddr in this wallet with non-default fee + fee = 0.00000001 # 1 puposhi fee will lead to some kind of rounding/truncation arithmetic + response = rpc.z_shieldcoinbase('*', zaddr1, fee, 1, donation) + opid = response['opid'] + print("opid=" + opid) + + shieldingValue = response['shieldingValue'] + # sanity check + assert_greater_than_or_equal( shieldingValue , 1.0 ) + + # TODO: this might not be enough time for slow machines, better + # solution would be to wait until the opid finishes + time.sleep(2) # give some time for the ztx to complete + + # confirm tx from above + rpc.generate(1) + self.sync_all() + + # get the txid + json = rpc.z_getoperationstatus() + # NOTE: this is index 1 because this test runs after the above test + # It would be better to specifically find the data for our opid + txid = json[1]['result']['txid'] + print("txid=" + txid) + + rpc.z_listunspent() + + # (300000000 - 1)*.95 + # 284999999.05 + # (300000000 - 1)*.05 + # 14999999.95 + # The above value will be truncated (not rounded) by casting from + # double to CAmount/int64_t which means the donation will be 14999999 + # and the sendAmount will be 300000000 - 14999999 = 285000001 + + expectedSendAmount = 285000001 + expectedDonationAmount = 14999999 + + # actually seeing this: 2.84990500 + 0.14999500 = 2.99990000 + # logging: donation=5, sendAmount=299999999, donationAmount=14999999 + + # lookup txid + rawtx1 = rpc.z_viewtransaction(txid) + + # TODO: set this up once for all tests since they all use the same zaddr + donation_zaddr = "zregtestsapling1y30nwg0clsu6gcyrnvht8hdyfk3vwtszlh6kc4z5hv9hmpxzg2g0nx7c60xeecggm9x9gma96t4" + + # there should be two outputs to different addresses, order is nondeterministic + if rawtx1['outputs'][0]['address'] == donation_zaddr: + donation_zout = 0 + other_zout = 1 + else: + donation_zout = 1 + other_zout = 0 + + assert_equal( rawtx1['outputs'][donation_zout]['address'] , donation_zaddr, 'correct zaddr gets donation') + assert_equal( rawtx1['outputs'][donation_zout]['valueZat'] , expectedDonationAmount, '5% donation sends correct donationAmount') + + assert_equal( rawtx1['outputs'][other_zout]['address'] , zaddr1, 'correct zaddr gets main amount') + assert_equal( rawtx1['outputs'][other_zout]['valueZat'] , expectedSendAmount, '5% donation sends correct sendAmount') + + #TODO: assert sum = 3 def run_test_custom(self): rpc = self.nodes[0] @@ -149,7 +222,7 @@ class ShieldCoinbaseDonationTest (BitcoinTestFramework): # (3 - fee)*0.05 expectedAmount1 = 14999500 - # lookup txid on node1 which should have received donation + # lookup txid rawtx1 = rpc.z_viewtransaction(txid) # there should be two outputs to different addresses, order is nondeterministic if rawtx1['outputs'][0]['address'] == testing_zaddr: From 6435cd51a63f841bdeff71cb49456546b30ac776 Mon Sep 17 00:00:00 2001 From: Duke Date: Wed, 15 Oct 2025 13:24:08 -0400 Subject: [PATCH 16/19] Use static_cast when calculating donation and add some debugging --- src/transaction_builder.cpp | 3 +++ src/wallet/asyncrpcoperation_shieldcoinbase.cpp | 15 ++++++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/transaction_builder.cpp b/src/transaction_builder.cpp index f942296a8..10f2383e5 100644 --- a/src/transaction_builder.cpp +++ b/src/transaction_builder.cpp @@ -54,6 +54,9 @@ void TransactionBuilder::AddSaplingOutput( CAmount value, std::array memo) { + if(fZdebug) { + LogPrintf("%s: adding output with value=%ld\n", __func__, value); + } auto note = libzcash::SaplingNote(to, value); outputs.emplace_back(ovk, note, memo); mtx.valueBalance -= value; diff --git a/src/wallet/asyncrpcoperation_shieldcoinbase.cpp b/src/wallet/asyncrpcoperation_shieldcoinbase.cpp index a11908230..34d87b58f 100644 --- a/src/wallet/asyncrpcoperation_shieldcoinbase.cpp +++ b/src/wallet/asyncrpcoperation_shieldcoinbase.cpp @@ -224,19 +224,24 @@ bool ShieldToAddress::operator()(const libzcash::SaplingPaymentAddress &zaddr) c throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid donation zaddr, Unknown address format: ") + dzaddr); } - //TODO: if donation>0, send X% of value to zaddr and Y% of of value to donatezaddr - // where X+Y=100% if(donation) { + //if donation>0, send X% of value to zaddr and Y% of of value to donatezaddr where X+Y=100% + // calculate donation as a double then convert to CAmount - CAmount donationAmount = (CAmount) ( ((double)donation/100)*sendAmount ); + double amount = (static_cast(donation)/100)*static_cast(sendAmount); + CAmount donationAmount = static_cast(amount); // add original recipient as first output, with sendAmount less the donation - m_op->builder_.AddSaplingOutput(ovk, zaddr, sendAmount - donationAmount); + CAmount newAmount = sendAmount - donationAmount; + m_op->builder_.AddSaplingOutput(ovk, zaddr, newAmount); auto donationZout = boost::get(donationZaddr); m_op->builder_.AddSaplingOutput(ovk, donationZout, donationAmount); - LogPrintf("%s: donation=%ld, sendAmount=%ld, donationAmount=%ld\n", __func__, donation, sendAmount, donationAmount); + if(fZdebug) { + LogPrintf("%s: donation=%ld, sendAmount=%ld, newAmount=%ld, donationAmount=%ld, newAmount+donationAmount=%ld fee=%ld\n", __func__, + donation, sendAmount, newAmount, donationAmount, newAmount+donationAmount, m_op->fee_); + } // zdust as third output, so donation txs are indistinguishable from // non-donation z_shieldcoinbase txs From 6069a495082a68a68d1e797ece2b28edfde705ae Mon Sep 17 00:00:00 2001 From: Duke Date: Wed, 15 Oct 2025 13:26:50 -0400 Subject: [PATCH 17/19] WIP donation tests These tests sometimes fail because sometimes a different amount is shielded (3 vs 3+fee) which makes the expected amounts of sendAmount and donationAmount incorrect. The code to calculate donations seems to work correctly but the tests need a bit more improvement. When the tests pass: DEBUG:RPC:<-43- {"remainingUTXOs": 1, "remainingValue": "3.00010000", "shieldingUTXOs": 1, "shieldingValue": "3.00000000", "donation": 5, "opid": "opid-868c81ad-885b-42b4-af85-dafa54e695a2"} When they fail: DEBUG:RPC:<-43- {"remainingUTXOs": 1, "remainingValue": "3.00000000", "shieldingUTXOs": 1, "shieldingValue": "3.00010000", "donation": 5, "opid": "opid-143309b6-e25a-45d6-9c0c-311f1f61de78"} --- qa/rpc-tests/shieldcoinbase_donation.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qa/rpc-tests/shieldcoinbase_donation.py b/qa/rpc-tests/shieldcoinbase_donation.py index 42e6b7329..efbf4939d 100755 --- a/qa/rpc-tests/shieldcoinbase_donation.py +++ b/qa/rpc-tests/shieldcoinbase_donation.py @@ -121,7 +121,7 @@ class ShieldCoinbaseDonationTest (BitcoinTestFramework): shieldingValue = response['shieldingValue'] # sanity check - assert_greater_than_or_equal( shieldingValue , 1.0 ) + assert_greater_than_or_equal( shieldingValue , 3.0 ) # TODO: this might not be enough time for slow machines, better # solution would be to wait until the opid finishes @@ -146,9 +146,9 @@ class ShieldCoinbaseDonationTest (BitcoinTestFramework): # 14999999.95 # The above value will be truncated (not rounded) by casting from # double to CAmount/int64_t which means the donation will be 14999999 - # and the sendAmount will be 300000000 - 14999999 = 285000001 + # and the sendAmount will be 300000000 - 1 (the fee) - 14999999 = 285000000 - expectedSendAmount = 285000001 + expectedSendAmount = 285000000 expectedDonationAmount = 14999999 # actually seeing this: 2.84990500 + 0.14999500 = 2.99990000 @@ -203,7 +203,7 @@ class ShieldCoinbaseDonationTest (BitcoinTestFramework): #wait_and_assert_operationid_status(rpc, opid) shieldingValue = response['shieldingValue'] - assert_greater_than_or_equal( shieldingValue , 1.0 ) + assert_greater_than_or_equal( shieldingValue , 3.0 ) time.sleep(2) # give some time for the ztx to complete From 37e35854ab8f1cecbdbe3735a1b2ef973b8eb8ef Mon Sep 17 00:00:00 2001 From: Duke Date: Thu, 16 Oct 2025 01:10:33 -0400 Subject: [PATCH 18/19] Make tests always pass by ensuring there is only 1 coinbase utxo to shield --- qa/rpc-tests/shieldcoinbase_donation.py | 28 +++++++++++-------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/qa/rpc-tests/shieldcoinbase_donation.py b/qa/rpc-tests/shieldcoinbase_donation.py index efbf4939d..cce21d85b 100755 --- a/qa/rpc-tests/shieldcoinbase_donation.py +++ b/qa/rpc-tests/shieldcoinbase_donation.py @@ -109,10 +109,6 @@ class ShieldCoinbaseDonationTest (BitcoinTestFramework): # sendAmount/donationAmount arithmetic leads to a situation where the # exact amount in satoshis must deal with truncation/rounding - # generate some new coinbase funds - rpc.generate(1) - self.sync_all() - # shield funds to a new zaddr in this wallet with non-default fee fee = 0.00000001 # 1 puposhi fee will lead to some kind of rounding/truncation arithmetic response = rpc.z_shieldcoinbase('*', zaddr1, fee, 1, donation) @@ -120,8 +116,9 @@ class ShieldCoinbaseDonationTest (BitcoinTestFramework): print("opid=" + opid) shieldingValue = response['shieldingValue'] - # sanity check - assert_greater_than_or_equal( shieldingValue , 3.0 ) + # sanity check. None of the expected values below will be correct if + # this is different + assert_equal( str(shieldingValue) , "3.00010000" ) # TODO: this might not be enough time for slow machines, better # solution would be to wait until the opid finishes @@ -140,19 +137,18 @@ class ShieldCoinbaseDonationTest (BitcoinTestFramework): rpc.z_listunspent() - # (300000000 - 1)*.95 - # 284999999.05 - # (300000000 - 1)*.05 - # 14999999.95 + # (300010000 - 1)*.05 + # 15000499.95 + # (300010000 - 1) - 15000499 + # 285009500 # The above value will be truncated (not rounded) by casting from - # double to CAmount/int64_t which means the donation will be 14999999 - # and the sendAmount will be 300000000 - 1 (the fee) - 14999999 = 285000000 + # double to CAmount/int64_t which means the donation will be 15000499 + # and the sendAmount will be 300010000 - 1 (the fee) - 15000499 = 285009500 - expectedSendAmount = 285000000 - expectedDonationAmount = 14999999 - # actually seeing this: 2.84990500 + 0.14999500 = 2.99990000 - # logging: donation=5, sendAmount=299999999, donationAmount=14999999 + # these values assume that 3.0001 was shielded + expectedSendAmount = 285009500 + expectedDonationAmount = 15000499 # lookup txid rawtx1 = rpc.z_viewtransaction(txid) From d206f28ae175c9b5007ca77418b84dd3996dcd7a Mon Sep 17 00:00:00 2001 From: Duke Date: Thu, 16 Oct 2025 01:10:46 -0400 Subject: [PATCH 19/19] Update z_shieldcoinbase rpc docs --- src/wallet/rpcwallet.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 7d816491b..24288df6a 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -5489,7 +5489,7 @@ UniValue z_shieldcoinbase(const UniValue& params, bool fHelp, const CPubKey& myp if (fHelp || params.size() < 2 || params.size() > 5) throw runtime_error( - "z_shieldcoinbase \"fromaddress\" \"tozaddress\" ( fee ) ( limit )\n" + "z_shieldcoinbase \"fromaddress\" \"tozaddress\" ( fee ) ( limit ) ( donation )\n" "\nShield transparent coinbase funds by sending to a shielded zaddr. This is an asynchronous operation and utxos" "\nselected for shielding will be locked. If there is an error, they are unlocked. The RPC call `listlockunspent`" "\ncan be used to return a list of locked utxos. The number of coinbase utxos selected for shielding can be limited" @@ -5502,15 +5502,16 @@ UniValue z_shieldcoinbase(const UniValue& params, bool fHelp, const CPubKey& myp "2. \"toaddress\" (string, required) The address is a zaddr.\n" "3. fee (numeric, optional, default=" + strprintf("%s", FormatMoney(SHIELD_COINBASE_DEFAULT_MINERS_FEE)) + ") The fee amount to attach to this transaction.\n" - "4. limit (numeric, optional, default=" + "4. limit (integer, optional, default=" + strprintf("%d", SHIELD_COINBASE_DEFAULT_LIMIT) + ") Limit on the maximum number of utxos to shield. Set to 0 to use as many as will fit in the transaction.\n" + "5. donation (integer, optional, default=0) Percentage of coinbase funds to donate. Must be between 0 and 10 inclusive.\n" "\nResult:\n" "{\n" - " \"remainingUTXOs\": xxx (numeric) Number of coinbase utxos still available for shielding.\n" + " \"remainingUTXOs\": xxx (integer) Number of coinbase utxos still available for shielding.\n" " \"remainingValue\": xxx (numeric) Value of coinbase utxos still available for shielding.\n" - " \"shieldingUTXOs\": xxx (numeric) Number of coinbase utxos being shielded.\n" - " \"shieldingValue\": xxx (numeric) Value of coinbase utxos being shielded.\n" - " \"opid\": xxx (string) An operationid to pass to z_getoperationstatus to get the result of the operation.\n" + " \"shieldingUTXOs\": xxx (integer) Number of coinbase utxos being shielded.\n" + " \"shieldingValue\": xxx (numeric) Value of coinbase utxos being shielded.\n" + " \"opid\": xxx (string) An operationid to pass to z_getoperationstatus to get the result of the operation.\n" "}\n" "\nExamples:\n" + HelpExampleCli("z_shieldcoinbase", "\"RD6GgnrMpPaTSMn8vai6yiGA7mN4QGPV\" \"zs14d8tc0hl9q0vg5l28uec5vk6sk34fkj2n8s7jalvw5fxpy6v39yn4s2ga082lymrkjk0x2nqg37\"")