Repository: zcash/zcash (language: rust)

Issue:
Remove the default unpaid action limit
The allowance for unpaid actions was not intended to be useful long-term. It was just a stopgap to allow us to deploy partial enforcement of ZIP 317 fees sooner, without that being blocked by "all" wallets supporting them for transaction creation.

It was understood when designing this mechanism that attackers would adapt to it, which they have, making the allowance essentially useless to the few legitimate users using wallets that are not paying these fees.

To avoid spending time on the changes to zcashd, I suggest just making `DEFAULT_BLOCK_UNPAID_ACTION_LIMIT` zero, which is a one-line change [1]. My impression is that mining pools do not override the default.

[1] with ~1300 lines of test changes (#6900). 😭


Discussion / hints:
Mempool metrics from `zcashd` for the past 4 days show a resumption in this behaviour:

![image](https://github.com/zcash/zcash/assets/4993799/93d4e30b-4e9f-401b-9ec3-b30d83cbb9c5)

Spot-checking the contents of the mempool just now, every spam transaction has 50 logical actions:
- 48 transparent outputs.
- 1 Sapling spend funding the transaction.
- 2 Sapling outputs (probably because the transaction builders force this).

So yes, they are directly targeting the current default unpaid action limit, and dropping the default to zero would resolve this.
Will reject it accepting tx to mempool and don’t do relay tx? Looks like no
hey. are most transparent wallets going to break after this change in fee? like trezor, ledger, coinomi, trust wallet, exodus etc? do they know they need to implement it? are they working on adding the ZIP317 support? 

if not, there will be probably many users who cant transact on their wallets without restoring in wallets that support it.

Fix patch:
diff --git a/doc/release-notes.md b/doc/release-notes.md
index 6fd5c2dd79f..93368dacc84 100644
--- a/doc/release-notes.md
+++ b/doc/release-notes.md
@@ -4,4 +4,18 @@ release-notes at release time)
 Notable changes
 ===============
 
+Mining
+------
+
+- The default setting of `-blockunpaidactionlimit` is now zero, which has
+  the effect of no longer allowing "unpaid actions" in [block production].
+  This adapts to current network conditions. If you have overridden this
+  setting as a miner, we recommend removing the override. This configuration
+  option may be removed entirely in a future release.
+
+[block production]: https://zips.z.cash/zip-0317#block-production
+
+Platform Support
+----------------
+
 - Windows builds have been fixed.
diff --git a/src/main.cpp b/src/main.cpp
index b15c2efc814..1fad8ee9a62 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -1921,7 +1921,8 @@ bool AcceptToMemoryPool(
                     ", which is over the limit of %d. The conventional fee for this transaction is %d " + MINOR_CURRENCY_UNIT,
                     tx.GetHash().ToString(), nSize, nModifiedFees, nModifiedFees - nFees, nUnpaidActionCount,
                     nTxUnpaidActionLimit, tx.GetConventionalFee());
-            return state.DoS(0, false, REJECT_INSUFFICIENTFEE, "tx unpaid action limit exceeded");
+            return state.DoS(0, false, REJECT_INSUFFICIENTFEE,
+                             strprintf("tx unpaid action limit exceeded: %d action(s) exceeds limit of %d", nUnpaidActionCount, nTxUnpaidActionLimit));
         }
 
         if (fRejectAbsurdFee && nFees > maxTxFee) {
diff --git a/src/policy/policy.h b/src/policy/policy.h
index 9b988db1d27..2d2ef4d31d8 100644
--- a/src/policy/policy.h
+++ b/src/policy/policy.h
@@ -71,7 +71,7 @@ static const unsigned int STANDARD_NOT_MANDATORY_VERIFY_FLAGS = STANDARD_SCRIPT_
  * set to 1000 zats, per ZIP 313.
  *
  * #6542 changed relaying policy to be more strict about enforcing minRelayTxFee.
- * It also allowed `-minrelaytxfee=0`, which we are using to avoid some test
+ * It also allowed `-minrelaytxfee=0`, which we were using to avoid some test
  * breakage. But if the dust threshold rate were still set to three times the
  * minRelayTxFee rate, then setting `-minrelaytxfee=0` would have the side effect
  * of setting the dust threshold to zero, which is not intended.
diff --git a/src/wallet/asyncrpcoperation_saplingmigration.cpp b/src/wallet/asyncrpcoperation_saplingmigration.cpp
index 19052d8c436..414ce0f4021 100644
--- a/src/wallet/asyncrpcoperation_saplingmigration.cpp
+++ b/src/wallet/asyncrpcoperation_saplingmigration.cpp
@@ -13,6 +13,7 @@
 
 #include <optional>
 #include <variant>
+#include <zip317.h>
 
 const int MIGRATION_EXPIRY_DELTA = 450;
 
@@ -159,13 +160,19 @@ bool AsyncRPCOperation_saplingmigration::main_impl() {
             }
         }
         assert(changeAddr.has_value());
-        // The amount chosen *includes* the default fee for this transaction, i.e.
-        // the value of the Sapling output will be 0.00001 ZEC less.
-        builder.SetFee(LEGACY_DEFAULT_FEE);
+
+        // The amount chosen includes the fee for this transaction, as it will be after
+        // adding the Sapling output. The first JoinSplit will consume two real inputs and
+        // each subsequent JoinSplit will consume one real input, usually (it is possible
+        // for the builder to use fewer JoinSplits but this only results in overestimating
+        // the fee). The number of Sapling outputs will be padded to 2.
+        auto fee = CalculateConventionalFee(2 * fromNotes.size() + 2);
+        builder.SetFee(fee);
+
         builder.AddSaplingOutput(
                 ovkForShieldingFromTaddr(seed),
                 migrationDestAddress,
-                amountToSend - LEGACY_DEFAULT_FEE,
+                amountToSend - fee,
                 std::nullopt);
         builder.SendChangeToSprout(changeAddr.value());
         CTransaction tx = builder.Build().GetTxOrThrow();
@@ -176,7 +183,7 @@ bool AsyncRPCOperation_saplingmigration::main_impl() {
         pwalletMain->AddPendingSaplingMigrationTx(tx);
         LogPrint("zrpcunsafe", "%s: Added pending migration transaction with txid=%s\n", getId(), tx.GetHash().ToString());
         ++numTxCreated;
-        amountMigrated += amountToSend - LEGACY_DEFAULT_FEE;
+        amountMigrated += amountToSend - fee;
         migrationTxIds.push_back(tx.GetHash().ToString());
     } while (numTxCreated < 5 && availableFunds > CENT);
 
diff --git a/src/zip317.h b/src/zip317.h
index 296ce1e2c8f..281c2419fb5 100644
--- a/src/zip317.h
+++ b/src/zip317.h
@@ -21,7 +21,7 @@ static const size_t P2PKH_STANDARD_OUTPUT_SIZE = 34;
 // Constants for block template construction.
 static const int64_t WEIGHT_RATIO_SCALE = INT64_C(10000000000000000);
 static const int64_t WEIGHT_RATIO_CAP = 4;
-static const size_t DEFAULT_BLOCK_UNPAID_ACTION_LIMIT = 50;
+static const size_t DEFAULT_BLOCK_UNPAID_ACTION_LIMIT = 0;
 
 /// Limit on the number of unpaid actions a transaction can have to be accepted to the mempool.
 static const size_t DEFAULT_TX_UNPAID_ACTION_LIMIT = DEFAULT_BLOCK_UNPAID_ACTION_LIMIT;


Test patch:
diff --git a/qa/pull-tester/rpc-tests.py b/qa/pull-tester/rpc-tests.py
index ebf18ad5c6a..5c3d68746e9 100755
--- a/qa/pull-tester/rpc-tests.py
+++ b/qa/pull-tester/rpc-tests.py
@@ -37,12 +37,11 @@
 
 FLAKY_SCRIPTS = [
     # These tests have intermittent failures that we haven't diagnosed yet.
-    'mempool_nu_activation.py',
+    'mempool_nu_activation.py', # this *may* be fixed
     'mempool_packages.py',
 ]
 
 BASE_SCRIPTS= [
-    # Scripts that are run by the travis build process
     # Longest test should go first, to favor running tests in parallel
     # vv Tests less than 5m vv
     'wallet.py',
@@ -94,6 +93,7 @@
     'wallet_sendmany_any_taddr.py',
     'wallet_treestate.py',
     'wallet_unified_change.py',
+    'wallet_zip317_default.py',
     'listtransactions.py',
     'mempool_resurrect_test.py',
     'txn_doublespend.py',
diff --git a/qa/rpc-tests/addressindex.py b/qa/rpc-tests/addressindex.py
index 6d5251e63df..bb2eedfe044 100755
--- a/qa/rpc-tests/addressindex.py
+++ b/qa/rpc-tests/addressindex.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright (c) 2019 The Zcash developers
+# Copyright (c) 2019-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 #
@@ -32,6 +32,8 @@
     OP_DROP,
 )
 
+from test_framework.zip317 import MARGINAL_FEE
+
 from test_framework.mininode import (
     COIN,
     CTransaction,
@@ -50,7 +52,6 @@ def __init__(self):
 
     def setup_network(self):
         base_args = [
-            '-minrelaytxfee=0',
             '-debug',
             '-txindex',
             '-experimentalfeatures',
@@ -374,7 +375,7 @@ def check_balance(node_index, address, expected_balance, expected_received=None)
         tx.vout = [
             CTxOut(1 * COIN, scriptPubKey),
             CTxOut(2 * COIN, scriptPubKey),
-            CTxOut(7 * COIN, scriptUnknown),
+            CTxOut(7 * COIN - 3 * MARGINAL_FEE, scriptUnknown),
         ]
         tx = self.nodes[0].signrawtransaction(hexlify(tx.serialize()).decode('utf-8'))
         txid = self.nodes[0].sendrawtransaction(tx['hex'], True)
diff --git a/qa/rpc-tests/feature_zip239.py b/qa/rpc-tests/feature_zip239.py
index b7b611990fd..12ae07fed91 100755
--- a/qa/rpc-tests/feature_zip239.py
+++ b/qa/rpc-tests/feature_zip239.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright (c) 2021 The Zcash developers
+# Copyright (c) 2021-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
@@ -21,7 +21,6 @@
     HEARTWOOD_BRANCH_ID,
     CANOPY_BRANCH_ID,
     NU5_BRANCH_ID,
-    LEGACY_DEFAULT_FEE,
     assert_equal,
     assert_false,
     assert_true,
@@ -31,6 +30,7 @@
     start_nodes,
     wait_and_assert_operationid_status,
 )
+from test_framework.zip317 import ZIP_317_FEE
 from tx_expiry_helper import TestNode
 
 import os.path
@@ -201,7 +201,7 @@ def run_test(self):
         opid = self.nodes[0].z_sendmany(sproutzaddr, [{
             'address': node1_taddr,
             'amount': 1,
-        }], 1, LEGACY_DEFAULT_FEE, 'AllowRevealedRecipients')
+        }], 1, ZIP_317_FEE, 'AllowRevealedRecipients')
         v4_txid = uint256_from_reversed_hex(
             wait_and_assert_operationid_status(self.nodes[0], opid)
         )
diff --git a/qa/rpc-tests/finalorchardroot.py b/qa/rpc-tests/finalorchardroot.py
index d5a478b8dfc..9077b2294b6 100755
--- a/qa/rpc-tests/finalorchardroot.py
+++ b/qa/rpc-tests/finalorchardroot.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright (c) 2022 The Zcash developers
+# Copyright (c) 2022-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
@@ -14,6 +14,7 @@
     start_nodes,
     wait_and_assert_operationid_status,
 )
+from test_framework.zip317 import conventional_fee, ZIP_317_FEE
 
 from decimal import Decimal
 
@@ -33,7 +34,6 @@ def __init__(self):
 
     def setup_network(self, split=False):
         self.nodes = start_nodes(self.num_nodes, self.options.tmpdir, extra_args=[[
-            '-minrelaytxfee=0',
             '-txindex', # Avoid JSONRPC error: No information available about transaction
             '-reindex', # Required due to enabling -txindex
             nuparams(NU5_BRANCH_ID, 200),
@@ -138,9 +138,9 @@ def run_test(self):
         assert_equal(acct0, addrRes0['account'])
         assert_equal(addrRes0['receiver_types'], ['orchard'])
         orchardAddr0 = addrRes0['address']
-        recipients = []
-        recipients.append({"address": orchardAddr0, "amount": Decimal('10')})
-        myopid = self.nodes[0].z_sendmany(taddr0, recipients, 1, 0, 'AllowRevealedSenders')
+        fee = conventional_fee(3)
+        recipients = [{"address": orchardAddr0, "amount": Decimal('10') - fee}]
+        myopid = self.nodes[0].z_sendmany(taddr0, recipients, 1, fee, 'AllowRevealedSenders')
         mytxid = wait_and_assert_operationid_status(self.nodes[0], myopid)
 
         self.sync_all()
@@ -158,8 +158,8 @@ def run_test(self):
         assert_equal(len(result["orchard"]["actions"]), 2)
         assert_equal(blk["trees"]["orchard"]["size"], 2)
 
-        # Since there is a now orchard shielded input in the blockchain,
-        # the orchard values should have changed
+        # Since there is now an Orchard shielded input in the blockchain,
+        # the Orchard values should have changed
         new_treestate = self.nodes[0].z_gettreestate(str(-1))
         assert_equal(new_treestate["orchard"]["commitments"]["finalRoot"], root)
         assert_equal(new_treestate["sprout"], treestate["sprout"])
@@ -192,8 +192,9 @@ def run_test(self):
         # Mine a block with a Sprout shielded tx and verify the final Orchard root does not change
         zaddr0 = self.nodes[0].listaddresses()[0]['sprout']['addresses'][0]
         assert_equal(self.nodes[0].z_getbalance(zaddr0), Decimal('50'))
+        fee = conventional_fee(3)
         recipients = [{"address": taddr0, "amount": Decimal('12.34')}]
-        opid = self.nodes[0].z_sendmany(zaddr0, recipients, 1, 0, 'AllowRevealedRecipients')
+        opid = self.nodes[0].z_sendmany(zaddr0, recipients, 1, fee, 'AllowRevealedRecipients')
         wait_and_assert_operationid_status(self.nodes[0], opid)
 
         self.sync_all()
@@ -202,7 +203,7 @@ def run_test(self):
 
         blk = self.nodes[0].getblock("214")
         assert_equal(len(blk["tx"]), 2)
-        assert_equal(self.nodes[0].z_getbalance(zaddr0), Decimal("37.66"))
+        assert_equal(self.nodes[0].z_getbalance(zaddr0), Decimal("37.66") - fee)
         assert_equal(root, blk["finalorchardroot"])
         assert_equal(blk["trees"]["orchard"]["size"], 2)
 
@@ -218,7 +219,7 @@ def run_test(self):
         # Mine a block with a Sapling shielded tx and verify the final Orchard root does not change
         saplingAddr1 = self.nodes[1].z_getnewaddress("sapling")
         recipients = [{"address": saplingAddr1, "amount": Decimal('2.34')}]
-        myopid = self.nodes[0].z_sendmany(zaddr0, recipients, 1, 0, 'AllowRevealedAmounts')
+        myopid = self.nodes[0].z_sendmany(zaddr0, recipients, 1, ZIP_317_FEE, 'AllowRevealedAmounts')
         mytxid = wait_and_assert_operationid_status(self.nodes[0], myopid)
 
         self.sync_all()
@@ -247,7 +248,7 @@ def run_test(self):
         assert_equal(addrRes1['receiver_types'], ['orchard'])
         orchardAddr1 = addrRes1['address']
         recipients = [{"address": orchardAddr1, "amount": Decimal('2.34')}]
-        myopid = self.nodes[0].z_sendmany(orchardAddr0, recipients, 1, 0)
+        myopid = self.nodes[0].z_sendmany(orchardAddr0, recipients, 1, ZIP_317_FEE)
         mytxid = wait_and_assert_operationid_status(self.nodes[0], myopid)
 
         self.sync_all()
@@ -275,9 +276,9 @@ def run_test(self):
 
         # Mine a block with an Orchard shielded sender and transparent recipient and verify the final Orchard root changes (because actions)
         taddr2 = self.nodes[0].getnewaddress()
-        recipients = []
-        recipients.append({"address": taddr2, "amount": Decimal('2.34')})
-        myopid = self.nodes[1].z_sendmany(orchardAddr1, recipients, 1, 0, 'AllowRevealedRecipients')
+        fee = conventional_fee(3)
+        recipients = [{"address": taddr2, "amount": Decimal('2.34') - fee}]
+        myopid = self.nodes[1].z_sendmany(orchardAddr1, recipients, 1, fee, 'AllowRevealedRecipients')
         mytxid = wait_and_assert_operationid_status(self.nodes[1], myopid)
 
         self.sync_all()
@@ -286,7 +287,8 @@ def run_test(self):
 
         blk = self.nodes[0].getblock("217")
         assert_equal(len(blk["tx"]), 2)
-        assert_equal(self.nodes[0].z_getbalance(taddr2), Decimal("2.34"))
+        assert_equal(self.nodes[0].z_getbalance(taddr2), Decimal("2.34") - fee)
+        assert_equal(self.nodes[1].z_getbalance(orchardAddr1), 0)
         assert root != blk["finalorchardroot"]
 
         # Verify there is a Orchard output description (its commitment was added to tree)
diff --git a/qa/rpc-tests/finalsaplingroot.py b/qa/rpc-tests/finalsaplingroot.py
index ed4914ac749..7cee9a84c13 100755
--- a/qa/rpc-tests/finalsaplingroot.py
+++ b/qa/rpc-tests/finalsaplingroot.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright (c) 2018 The Zcash developers
+# Copyright (c) 2018-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
@@ -14,6 +14,7 @@
     start_nodes,
     wait_and_assert_operationid_status,
 )
+from test_framework.zip317 import conventional_fee, ZIP_317_FEE
 
 from decimal import Decimal
 
@@ -33,7 +34,6 @@ def __init__(self):
 
     def setup_network(self, split=False):
         self.nodes = start_nodes(self.num_nodes, self.options.tmpdir, extra_args=[[
-            '-minrelaytxfee=0',
             '-txindex', # Avoid JSONRPC error: No information available about transaction
             '-reindex', # Required due to enabling -txindex
             nuparams(NU5_BRANCH_ID, 210),
@@ -105,9 +105,9 @@ def run_test(self):
         # Node 0 shields some funds
         taddr0 = get_coinbase_address(self.nodes[0])
         saplingAddr0 = self.nodes[0].z_getnewaddress('sapling')
-        recipients = []
-        recipients.append({"address": saplingAddr0, "amount": Decimal('10')})
-        myopid = self.nodes[0].z_sendmany(taddr0, recipients, 1, 0, 'AllowRevealedSenders')
+        fee = conventional_fee(3)
+        recipients = [{"address": saplingAddr0, "amount": Decimal('10') - fee}]
+        myopid = self.nodes[0].z_sendmany(taddr0, recipients, 1, fee, 'AllowRevealedSenders')
         mytxid = wait_and_assert_operationid_status(self.nodes[0], myopid)
 
         self.sync_all()
@@ -125,8 +125,8 @@ def run_test(self):
         assert_equal(len(result["vShieldedOutput"]), 2) # Non-coinbase bundles are padded
         assert_equal(blk["trees"]["sapling"]["size"], 2)
 
-        # Since there is a now sapling shielded input in the blockchain,
-        # the sapling values should have changed
+        # Since there is now a Sapling shielded input in the blockchain,
+        # the Sapling values should have changed
         new_treestate = self.nodes[0].z_gettreestate(str(-1))
         assert_equal(new_treestate["sapling"]["commitments"]["finalRoot"], root)
         assert_equal(new_treestate["sprout"], treestate["sprout"])
@@ -159,8 +159,9 @@ def run_test(self):
         # Mine a block with a Sprout shielded tx and verify the final Sapling root does not change
         zaddr0 = self.nodes[0].listaddresses()[0]['sprout']['addresses'][0]
         assert_equal(self.nodes[0].z_getbalance(zaddr0), Decimal('50'))
+        fee = conventional_fee(3)
         recipients = [{"address": taddr0, "amount": Decimal('12.34')}]
-        opid = self.nodes[0].z_sendmany(zaddr0, recipients, 1, 0, 'AllowRevealedRecipients')
+        opid = self.nodes[0].z_sendmany(zaddr0, recipients, 1, fee, 'AllowRevealedRecipients')
         wait_and_assert_operationid_status(self.nodes[0], opid)
 
         self.sync_all()
@@ -169,7 +170,7 @@ def run_test(self):
 
         blk = self.nodes[0].getblock("204")
         assert_equal(len(blk["tx"]), 2)
-        assert_equal(self.nodes[0].z_getbalance(zaddr0), Decimal("37.66"))
+        assert_equal(self.nodes[0].z_getbalance(zaddr0), Decimal("37.66") - fee)
         assert_equal(root, blk["finalsaplingroot"])
         assert_equal(blk["trees"]["sapling"]["size"], 2)
 
@@ -185,7 +186,7 @@ def run_test(self):
         # Mine a block with a Sapling shielded recipient and verify the final Sapling root changes
         saplingAddr1 = self.nodes[1].z_getnewaddress("sapling")
         recipients = [{"address": saplingAddr1, "amount": Decimal('2.34')}]
-        myopid = self.nodes[0].z_sendmany(saplingAddr0, recipients, 1, 0)
+        myopid = self.nodes[0].z_sendmany(saplingAddr0, recipients, 1, ZIP_317_FEE)
         mytxid = wait_and_assert_operationid_status(self.nodes[0], myopid)
 
         self.sync_all()
@@ -212,9 +213,9 @@ def run_test(self):
 
         # Mine a block with a Sapling shielded sender and transparent recipient.
         taddr2 = self.nodes[0].getnewaddress()
-        recipients = []
-        recipients.append({"address": taddr2, "amount": Decimal('2.34')})
-        myopid = self.nodes[1].z_sendmany(saplingAddr1, recipients, 1, 0, 'AllowRevealedRecipients')
+        fee = conventional_fee(3)
+        recipients = [{"address": taddr2, "amount": Decimal('2.34') - fee}]
+        myopid = self.nodes[1].z_sendmany(saplingAddr1, recipients, 1, fee, 'AllowRevealedRecipients')
         mytxid = wait_and_assert_operationid_status(self.nodes[1], myopid)
 
         self.sync_all()
@@ -224,13 +225,11 @@ def run_test(self):
         blk = self.nodes[0].getblock("206")
         assert_equal(len(blk["tx"]), 2)
         assert_equal(mytxid, blk["tx"][1])
-        assert_equal(self.nodes[0].z_getbalance(taddr2), Decimal("2.34"))
+        assert_equal(self.nodes[0].z_getbalance(taddr2), Decimal("2.34") - fee)
         assert_equal(self.nodes[1].z_getbalance(saplingAddr1), 0)
 
         # Verify the final Sapling root changes (because the Sapling bundle was padded
         # with 2 dummy outputs).
-        print(self.nodes[0].getrawtransaction(blk["tx"][0]))
-        print(self.nodes[0].getrawtransaction(blk["tx"][1]))
         root = blk["finalsaplingroot"]
         assert root != self.nodes[0].getblock("205")["finalsaplingroot"]
         assert_equal(blk["trees"]["sapling"]["size"], 6)
diff --git a/qa/rpc-tests/getblocktemplate.py b/qa/rpc-tests/getblocktemplate.py
index 7b7c3ec6fa0..0d97b7a9021 100755
--- a/qa/rpc-tests/getblocktemplate.py
+++ b/qa/rpc-tests/getblocktemplate.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright (c) 2016 The Zcash developers
+# Copyright (c) 2016-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
@@ -9,7 +9,6 @@
 from test_framework.util import (
     assert_equal,
     CANOPY_BRANCH_ID,
-    LEGACY_DEFAULT_FEE,
     NU5_BRANCH_ID,
     get_coinbase_address,
     hex_str_to_bytes,
@@ -24,6 +23,8 @@
 from test_framework.blocktools import (
     create_block
 )
+from test_framework.zip317 import conventional_fee, ZIP_317_FEE
+
 from decimal import Decimal
 
 class GetBlockTemplateTest(BitcoinTestFramework):
@@ -52,13 +53,14 @@ def add_nu5_v4_tx_to_mempool(self):
         node = self.node
         # sprout to transparent (v4)
         recipients = [{"address": self.transparent_addr, "amount": Decimal('0.1')}]
-        myopid = node.z_sendmany(self.sprout_addr, recipients, 1, LEGACY_DEFAULT_FEE, 'AllowRevealedRecipients')
+        myopid = node.z_sendmany(self.sprout_addr, recipients, 1, ZIP_317_FEE, 'AllowRevealedRecipients')
         wait_and_assert_operationid_status(node, myopid)
 
     def add_nu5_v5_tx_to_mempool(self):
         node = self.node
-        recipients = [{"address": self.unified_addr, "amount": Decimal('9.99999')}]
-        myopid = node.z_sendmany(get_coinbase_address(node), recipients, 1, LEGACY_DEFAULT_FEE, 'AllowRevealedSenders')
+        fee = conventional_fee(3)
+        recipients = [{"address": self.unified_addr, "amount": Decimal('10') - fee}]
+        myopid = node.z_sendmany(get_coinbase_address(node), recipients, 1, fee, 'AllowRevealedSenders')
         wait_and_assert_operationid_status(node, myopid)
 
     def add_transparent_tx_to_mempool(self):
@@ -196,7 +198,6 @@ def run_test(self):
 
         print("- block with 6 Orchard transactions (plus coinbase)")
         for i in range(0, 6):
-            print(str(node.z_getbalance(self.transparent_addr)))
             self.add_nu5_v5_tx_to_mempool()
         self.gbt_submitblock(True)
 
diff --git a/qa/rpc-tests/invalidtxrequest.py b/qa/rpc-tests/invalidtxrequest.py
index 369bc4d9c29..1b2d4cf6322 100755
--- a/qa/rpc-tests/invalidtxrequest.py
+++ b/qa/rpc-tests/invalidtxrequest.py
@@ -1,15 +1,17 @@
 #!/usr/bin/env python3
-#
+# Copyright (c) 2015-2016 The Bitcoin Core developers
+# Copyright (c) 2021-2024 The Zcash developers
 # Distributed under the MIT/X11 software license, see the accompanying
 # file COPYING or http://www.opensource.org/licenses/mit-license.php.
 #
 
 from test_framework.test_framework import ComparisonTestFramework
 from test_framework.comptool import TestManager, TestInstance, RejectResult
-from test_framework.mininode import NetworkThread
+from test_framework.mininode import NetworkThread, COIN
 from test_framework.blocktools import create_block, create_coinbase, create_transaction
-import time
+from test_framework.zip317 import MINIMUM_FEE
 
+import time
 
 '''
 In this test we connect to one node over p2p, and test tx requests.
@@ -19,7 +21,6 @@
 class InvalidTxRequestTest(ComparisonTestFramework):
     def __init__(self):
         super().__init__()
-        self.additional_args = ['-minrelaytxfee=0']
 
     ''' Can either run this test as 1 node with expected answers, or two and compare them. 
         Change the "outcome" variable from each TestInstance object to only do the comparison. '''
@@ -64,7 +65,7 @@ def get_tests(self):
 
         # b'\x64' is OP_NOTIF
         # Transaction will be rejected with code 16 (REJECT_INVALID)
-        tx1 = create_transaction(self.block1.vtx[0], 0, b'\x64', 10*100000000)
+        tx1 = create_transaction(self.block1.vtx[0], 0, b'\x64', 10*COIN - MINIMUM_FEE)
         yield TestInstance([[tx1, RejectResult(16, b'mandatory-script-verify-flag-failed')]])
 
         # TODO: test further transactions...
diff --git a/qa/rpc-tests/mempool_limit.py b/qa/rpc-tests/mempool_limit.py
index 874f238b5e0..00b6e9340c2 100755
--- a/qa/rpc-tests/mempool_limit.py
+++ b/qa/rpc-tests/mempool_limit.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 # Copyright (c) 2014-2016 The Bitcoin Core developers
-# Copyright (c) 2019-2022 The Zcash developers
+# Copyright (c) 2019-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
@@ -67,28 +67,24 @@ def run_test(self):
         zaddr1 = self.nodes[0].z_getnewaddress('sapling')
         zaddr2 = self.nodes[0].z_getnewaddress('sapling')
         zaddr3 = self.nodes[0].z_getnewaddress('sapling')
-        fee = conventional_fee(2)
 
         print("Filling mempool...")
-        opid1 = self.nodes[1].z_sendmany(
-            get_coinbase_address(self.nodes[1]),
-            [{"address": zaddr1, "amount": Decimal('10.0') - fee}],
-            1, fee, 'AllowRevealedSenders')
+        fee = conventional_fee(3)
+        recipients = [{"address": zaddr1, "amount": Decimal('10.0') - fee}]
+        opid1 = self.nodes[1].z_sendmany(get_coinbase_address(self.nodes[1]), recipients, 1, fee, 'AllowRevealedSenders')
         wait_and_assert_operationid_status(self.nodes[1], opid1)
-        opid2 = self.nodes[2].z_sendmany(
-            get_coinbase_address(self.nodes[2]),
-            [{"address": zaddr2, "amount": Decimal('10.0') - fee}],
-            1, fee, 'AllowRevealedSenders')
+
+        recipients = [{"address": zaddr2, "amount": Decimal('10.0') - fee}]
+        opid2 = self.nodes[2].z_sendmany(get_coinbase_address(self.nodes[2]), recipients, 1, fee, 'AllowRevealedSenders')
         wait_and_assert_operationid_status(self.nodes[2], opid2)
+
         self.sync_all()
 
         self.check_mempool_sizes(2)
 
         print("Adding one more transaction...")
-        opid3 = self.nodes[3].z_sendmany(
-            get_coinbase_address(self.nodes[3]),
-            [{"address": zaddr3, "amount": Decimal('10.0') - fee}],
-            1, fee, 'AllowRevealedSenders')
+        recipients = [{"address": zaddr3, "amount": Decimal('10.0') - fee}]
+        opid3 = self.nodes[3].z_sendmany(get_coinbase_address(self.nodes[3]), recipients, 1, fee, 'AllowRevealedSenders')
         wait_and_assert_operationid_status(self.nodes[3], opid3)
         # The mempools are no longer guaranteed to be in a consistent state, so we cannot sync
         sleep(5)
@@ -106,9 +102,10 @@ def run_test(self):
         print("Checking mempool size reset after block mined...")
         self.check_mempool_sizes(0)
         zaddr4 = self.nodes[0].z_getnewaddress('sapling')
-        opid4 = self.nodes[0].z_sendmany(zaddr1, [{"address": zaddr4, "amount": Decimal('10.0') - 2*fee}], 1)
+        recipients = [{"address": zaddr4, "amount": Decimal('10.0') - 2*fee}]
+        opid4 = self.nodes[0].z_sendmany(zaddr1, recipients, 1, fee)
         wait_and_assert_operationid_status(self.nodes[0], opid4)
-        opid5 = self.nodes[0].z_sendmany(zaddr2, [{"address": zaddr4, "amount": Decimal('10.0') - 2*fee}], 1)
+        opid5 = self.nodes[0].z_sendmany(zaddr2, recipients, 1, fee)
         wait_and_assert_operationid_status(self.nodes[0], opid5)
         self.sync_all()
 
diff --git a/qa/rpc-tests/mempool_nu_activation.py b/qa/rpc-tests/mempool_nu_activation.py
index 43c8691a3f5..76ae41754f3 100755
--- a/qa/rpc-tests/mempool_nu_activation.py
+++ b/qa/rpc-tests/mempool_nu_activation.py
@@ -1,20 +1,21 @@
 #!/usr/bin/env python3
-# Copyright (c) 2018 The Zcash developers
+# Copyright (c) 2018-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
 from test_framework.test_framework import BitcoinTestFramework
-from test_framework.mininode import NU5_PROTO_VERSION
 from test_framework.util import (
     BLOSSOM_BRANCH_ID,
     CANOPY_BRANCH_ID,
     HEARTWOOD_BRANCH_ID,
     NU5_BRANCH_ID,
+    NU6_BRANCH_ID,
     assert_equal, assert_true,
     nuparams,
     start_node, connect_nodes, wait_and_assert_operationid_status,
     get_coinbase_address
 )
+from test_framework.zip317 import conventional_fee
 
 from decimal import Decimal
 
@@ -30,18 +31,17 @@ def __init__(self):
 
     def setup_network(self):
         args = [
-            '-minrelaytxfee=0',
             '-checkmempool',
             '-debug=mempool',
             '-blockmaxsize=4000',
             '-allowdeprecated=getnewaddress',
-            '-allowdeprecated=legacy_privacy',
             '-allowdeprecated=z_getnewaddress',
             '-allowdeprecated=z_getbalance',
             nuparams(BLOSSOM_BRANCH_ID, 200),
             nuparams(HEARTWOOD_BRANCH_ID, 210),
             nuparams(CANOPY_BRANCH_ID, 220),
             nuparams(NU5_BRANCH_ID, 230),
+            nuparams(NU6_BRANCH_ID, 240),
         ]
         self.nodes = []
         self.nodes.append(start_node(0, self.options.tmpdir, args))
@@ -62,9 +62,13 @@ def run_test(self):
         # Shield some ZEC
         node1_taddr = get_coinbase_address(self.nodes[1])
         node0_zaddr = self.nodes[0].z_getnewaddress('sapling')
-        recipients = [{'address': node0_zaddr, 'amount': Decimal('10')}]
-        myopid = self.nodes[1].z_sendmany(node1_taddr, recipients, 1, 0)
-        print(wait_and_assert_operationid_status(self.nodes[1], myopid))
+        coinbase_fee = conventional_fee(3)
+        recipients = [{'address': node0_zaddr, 'amount': Decimal('10') - coinbase_fee}]
+        myopid = self.nodes[1].z_sendmany(node1_taddr, recipients, 1, coinbase_fee, 'AllowRevealedSenders')
+        wait_and_assert_operationid_status(self.nodes[1], myopid)
+        node0_balance = Decimal('10') - coinbase_fee
+        fee = conventional_fee(2)
+
         self.sync_all()
         self.nodes[0].generate(1)
         self.sync_all()
@@ -79,14 +83,22 @@ def nu_activation_checks():
             assert_equal(set(self.nodes[0].getrawmempool()), set())
 
             # Check node 0 shielded balance
-            assert_equal(self.nodes[0].z_getbalance(node0_zaddr), Decimal('10'))
+            assert_equal(self.nodes[0].z_getbalance(node0_zaddr), node0_balance)
 
             # Fill the mempool with more transactions than can fit into 4 blocks
+            # (note `-blockmaxsize=4000` in the node arguments).
             node0_taddr = self.nodes[0].getnewaddress()
             x_txids = []
+            print("Filling mempool", end="", flush=True)
             while self.nodes[1].getmempoolinfo()['bytes'] < 8 * 4000:
                 x_txids.append(self.nodes[1].sendtoaddress(node0_taddr, Decimal('0.001')))
+                if len(x_txids) % 10 == 0:
+                    print(".", end="", flush=True)
+                    # this sync is important for reliability
+                    self.sync_all()
+
             self.sync_all()
+            print(" done")
 
             # Spends should be in the mempool
             x_mempool = set(self.nodes[0].getrawmempool())
@@ -147,8 +159,8 @@ def nu_activation_checks():
             self.sync_all()
 
             # Create a shielded Y transaction
-            recipients = [{'address': node0_zaddr, 'amount': Decimal('10')}]
-            myopid = self.nodes[0].z_sendmany(node0_zaddr, recipients, 1,0)
+            recipients = [{'address': node0_zaddr, 'amount': node0_balance - fee}]
+            myopid = self.nodes[0].z_sendmany(node0_zaddr, recipients, 1, fee)
             shielded = wait_and_assert_operationid_status(self.nodes[0], myopid)
             assert(shielded != None)
             y_txids.append(shielded)
@@ -179,7 +191,7 @@ def nu_activation_checks():
             assert_equal(set(self.nodes[1].getrawmempool()), set(y_txids))
 
             # Node 0 note should be spendable again
-            assert_equal(self.nodes[0].z_getbalance(node0_zaddr), Decimal('10'))
+            assert_equal(self.nodes[0].z_getbalance(node0_zaddr), node0_balance)
 
             # Reconsider block H - 1.
             self.nodes[0].reconsiderblock(block_hm1)
@@ -189,28 +201,31 @@ def nu_activation_checks():
             self.nodes[1].generate(6)
             self.sync_all()
 
-        net_version = self.nodes[0].getnetworkinfo()["protocolversion"]
-
         print('Testing Sapling -> Blossom activation boundary')
-        # Current height = 195
+        assert_equal(self.nodes[0].getblockcount(), 195)
         nu_activation_checks()
-        # Current height = 205
+        node0_balance -= fee
+        assert_equal(self.nodes[0].getblockcount(), 205)
 
         print('Testing Blossom -> Heartwood activation boundary')
         nu_activation_checks()
-        # Current height = 215
+        node0_balance -= fee
+        assert_equal(self.nodes[0].getblockcount(), 215)
 
         print('Testing Heartwood -> Canopy activation boundary')
         nu_activation_checks()
-        # Current height = 225
-
-        if net_version < NU5_PROTO_VERSION:
-            print("Node's block index is not NU5-aware, skipping remaining tests")
-            return
+        node0_balance -= fee
+        assert_equal(self.nodes[0].getblockcount(), 225)
 
         print('Testing Canopy -> NU5 activation boundary')
         nu_activation_checks()
-        # Current height = 235
+        node0_balance -= fee
+        assert_equal(self.nodes[0].getblockcount(), 235)
+
+        print('Testing NU5 -> NU6 activation boundary')
+        nu_activation_checks()
+        node0_balance -= fee
+        assert_equal(self.nodes[0].getblockcount(), 245)
 
 if __name__ == '__main__':
     MempoolUpgradeActivationTest().main()
diff --git a/qa/rpc-tests/mempool_packages.py b/qa/rpc-tests/mempool_packages.py
index 73d224ca589..7b3882df3c0 100755
--- a/qa/rpc-tests/mempool_packages.py
+++ b/qa/rpc-tests/mempool_packages.py
@@ -1,5 +1,6 @@
 #!/usr/bin/env python3
 # Copyright (c) 2014-2016 The Bitcoin Core developers
+# Copyright (c) 2018-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or http://www.opensource.org/licenses/mit-license.php.
 
@@ -28,7 +29,6 @@ class MempoolPackagesTest(BitcoinTestFramework):
     def setup_network(self):
         base_args = [
             '-limitdescendantcount=%d' % (self.limitdescendantcount,),
-            '-minrelaytxfee=0',
             '-maxorphantx=%d' % (self.limitdescendantcount,),
             '-debug',
             '-allowdeprecated=getnewaddress',
@@ -63,7 +63,7 @@ def run_test(self):
         vout = utxo[0]['vout']
         value = utxo[0]['amount']
 
-        fee = conventional_fee(2)
+        fee = conventional_fee(10)
         # 100 transactions off a confirmed tx should be fine
         chain = []
         for i in range(100):
diff --git a/qa/rpc-tests/mempool_reorg.py b/qa/rpc-tests/mempool_reorg.py
index 06e9d1f5b54..8d9f01374cf 100755
--- a/qa/rpc-tests/mempool_reorg.py
+++ b/qa/rpc-tests/mempool_reorg.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 # Copyright (c) 2014-2016 The Bitcoin Core developers
-# Copyright (c) 2019-2022 The Zcash developers
+# Copyright (c) 2019-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
@@ -9,9 +9,12 @@
 # that spend (directly or indirectly) coinbase transactions.
 #
 
+from decimal import Decimal
+
 from test_framework.test_framework import BitcoinTestFramework
 from test_framework.authproxy import JSONRPCException
 from test_framework.util import assert_equal, assert_raises, start_node, connect_nodes
+from test_framework.zip317 import conventional_fee
 
 
 # Create one-input, one-output, no-fee transaction:
@@ -24,7 +27,6 @@ def __init__(self):
 
     def setup_network(self):
         args = [
-            '-minrelaytxfee=0',
             '-checkmempool',
             '-debug=mempool',
             '-allowdeprecated=getnewaddress',
@@ -61,12 +63,13 @@ def run_test(self):
         # and make sure the mempool code behaves correctly.
         b = [ self.nodes[0].getblockhash(n) for n in range(101, 105) ]
         coinbase_txids = [ self.nodes[0].getblock(h)['tx'][0] for h in b ]
-        spend_101_raw = self.create_tx(coinbase_txids[1], node1_address, 10)
-        spend_102_raw = self.create_tx(coinbase_txids[2], node0_address, 10)
-        spend_103_raw = self.create_tx(coinbase_txids[3], node0_address, 10)
+        fee = conventional_fee(2)
+        spend_101_raw = self.create_tx(coinbase_txids[1], node1_address, Decimal('10') - fee)
+        spend_102_raw = self.create_tx(coinbase_txids[2], node0_address, Decimal('10') - fee)
+        spend_103_raw = self.create_tx(coinbase_txids[3], node0_address, Decimal('10') - fee)
 
         # Create a block-height-locked transaction which will be invalid after reorg
-        timelock_tx = self.nodes[0].createrawtransaction([{"txid": coinbase_txids[0], "vout": 0}], {node0_address: 10})
+        timelock_tx = self.nodes[0].createrawtransaction([{"txid": coinbase_txids[0], "vout": 0}], {node0_address: Decimal('10') - fee})
         # Set the time lock, ensuring we don't clobber the rest of the Sapling v4 tx format
         timelock_tx = timelock_tx.replace("ffffffff", "11111111", 1)
         timelock_tx = timelock_tx[:-38] + hex(self.nodes[0].getblockcount() + 2)[2:] + "000000" + timelock_tx[-30:]
@@ -80,8 +83,8 @@ def run_test(self):
         assert_raises(JSONRPCException, self.nodes[0].sendrawtransaction, timelock_tx)
 
         # Create 102_1 and 103_1:
-        spend_102_1_raw = self.create_tx(spend_102_id, node1_address, 10)
-        spend_103_1_raw = self.create_tx(spend_103_id, node1_address, 10)
+        spend_102_1_raw = self.create_tx(spend_102_id, node1_address, Decimal('10') - 2*fee)
+        spend_103_1_raw = self.create_tx(spend_103_id, node1_address, Decimal('10') - 2*fee)
 
         # Broadcast and mine 103_1:
         spend_103_1_id = self.nodes[0].sendrawtransaction(spend_103_1_raw)
diff --git a/qa/rpc-tests/mempool_resurrect_test.py b/qa/rpc-tests/mempool_resurrect_test.py
index 1d317501818..1b0264a0f2f 100755
--- a/qa/rpc-tests/mempool_resurrect_test.py
+++ b/qa/rpc-tests/mempool_resurrect_test.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 # Copyright (c) 2014-2016 The Bitcoin Core developers
-# Copyright (c) 2016-2022 The Zcash developers
+# Copyright (c) 2016-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
@@ -10,7 +10,8 @@
 #
 
 from test_framework.test_framework import BitcoinTestFramework
-from test_framework.util import assert_equal, start_node, LEGACY_DEFAULT_FEE
+from test_framework.util import assert_equal, start_node
+from test_framework.zip317 import conventional_fee
 
 from decimal import Decimal
 
@@ -25,7 +26,6 @@ def __init__(self):
     def setup_network(self):
         # Just need one node for this test
         args = [
-            '-minrelaytxfee=0',
             '-checkmempool',
             '-debug=mempool',
             '-allowdeprecated=getnewaddress',
@@ -56,13 +56,14 @@ def run_test(self):
 
         b = [ self.nodes[0].getblockhash(n) for n in range(1, 4) ]
         coinbase_txids = [ self.nodes[0].getblock(h)['tx'][0] for h in b ]
-        spends1_raw = [ self.create_tx(txid, node0_address, 10) for txid in coinbase_txids ]
+        fee = conventional_fee(1)
+        spends1_raw = [ self.create_tx(txid, node0_address, Decimal('10') - fee) for txid in coinbase_txids ]
         spends1_id = [ self.nodes[0].sendrawtransaction(tx) for tx in spends1_raw ]
 
         blocks = []
         blocks.extend(self.nodes[0].generate(1))
 
-        spends2_raw = [ self.create_tx(txid, node0_address, Decimal('10.0') - LEGACY_DEFAULT_FEE) for txid in spends1_id ]
+        spends2_raw = [ self.create_tx(txid, node0_address, Decimal('10') - 2*fee) for txid in spends1_id ]
         spends2_id = [ self.nodes[0].sendrawtransaction(tx) for tx in spends2_raw ]
 
         blocks.extend(self.nodes[0].generate(1))
diff --git a/qa/rpc-tests/mempool_spendcoinbase.py b/qa/rpc-tests/mempool_spendcoinbase.py
index 43da9f90590..809bdf47780 100755
--- a/qa/rpc-tests/mempool_spendcoinbase.py
+++ b/qa/rpc-tests/mempool_spendcoinbase.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 # Copyright (c) 2014-2016 The Bitcoin Core developers
-# Copyright (c) 2016-2022 The Zcash developers
+# Copyright (c) 2016-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
@@ -14,10 +14,13 @@
 # but less mature coinbase spends are NOT.
 #
 
+from decimal import Decimal
+
 from test_framework.test_framework import BitcoinTestFramework
 from test_framework.authproxy import JSONRPCException
 from test_framework.util import assert_equal, assert_greater_than, assert_raises, \
     start_node
+from test_framework.zip317 import conventional_fee
 
 
 # Create one-input, one-output, no-fee transaction:
@@ -30,7 +33,6 @@ def __init__(self):
     def setup_network(self):
         # Just need one node for this test
         args = [
-            '-minrelaytxfee=0',
             '-checkmempool',
             '-debug=mempool',
             '-allowdeprecated=getnewaddress',
@@ -57,7 +59,8 @@ def run_test(self):
         # is too immature to spend.
         b = [ self.nodes[0].getblockhash(n) for n in range(101, 103) ]
         coinbase_txids = [ self.nodes[0].getblock(h)['tx'][0] for h in b ]
-        spends_raw = [ self.create_tx(txid, node0_address, 10) for txid in coinbase_txids ]
+        fee = conventional_fee(1)
+        spends_raw = [ self.create_tx(txid, node0_address, Decimal('10') - fee) for txid in coinbase_txids ]
 
         spend_101_id = self.nodes[0].sendrawtransaction(spends_raw[0])
 
diff --git a/qa/rpc-tests/mempool_tx_expiry.py b/qa/rpc-tests/mempool_tx_expiry.py
index 9359ded5193..3bec6abd92e 100755
--- a/qa/rpc-tests/mempool_tx_expiry.py
+++ b/qa/rpc-tests/mempool_tx_expiry.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright (c) 2018 The Zcash developers
+# Copyright (c) 2018-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
@@ -11,7 +11,7 @@
 from test_framework.test_framework import BitcoinTestFramework
 from test_framework.util import assert_equal, \
     connect_nodes_bi, sync_blocks, start_nodes, \
-    wait_and_assert_operationid_status, LEGACY_DEFAULT_FEE
+    wait_and_assert_operationid_status
 from test_framework.zip317 import conventional_fee
 
 from decimal import Decimal
@@ -23,7 +23,6 @@ class MempoolTxExpiryTest(BitcoinTestFramework):
 
     def setup_nodes(self):
         return start_nodes(self.num_nodes, self.options.tmpdir, extra_args=[[
-            '-minrelaytxfee=0',
             '-txexpirydelta=%d' % TX_EXPIRY_DELTA,
             '-debug=mempool',
             '-allowdeprecated=getnewaddress',
@@ -43,7 +42,7 @@ def run_test(self):
         self.split_network()
 
         # Test dependent txs
-        firstTx = self.nodes[0].sendtoaddress(alice, 0.1)
+        firstTx = self.nodes[0].sendtoaddress(alice, Decimal('0.1'))
         firstTxInfo = self.nodes[0].getrawtransaction(firstTx, 1)
         assert_equal(firstTxInfo["version"], 4)
         assert_equal(firstTxInfo["overwintered"], True)
@@ -56,7 +55,8 @@ def run_test(self):
                 vout = outpoint
                 break
         inputs = [{'txid': firstTx, 'vout': vout['n'], 'scriptPubKey': vout['scriptPubKey']['hex']}]
-        outputs = {alice: 0.1}
+        fee = conventional_fee(2)
+        outputs = {alice: Decimal('0.1') - fee}
         rawTx = self.nodes[0].createrawtransaction(inputs, outputs)
         rawTxSigned = self.nodes[0].signrawtransaction(rawTx)
         assert(rawTxSigned['complete'])
@@ -79,7 +79,8 @@ def run_test(self):
         assert_equal(set(self.nodes[2].getrawmempool()), set())
 
         ## Shield one of Alice's coinbase funds to her zaddr
-        res = self.nodes[0].z_shieldcoinbase("*", z_alice, LEGACY_DEFAULT_FEE, 1)
+        coinbase_fee = conventional_fee(3)
+        res = self.nodes[0].z_shieldcoinbase("*", z_alice, coinbase_fee, 1)
         wait_and_assert_operationid_status(self.nodes[0], res['opid'])
         self.nodes[0].generate(1)
         self.sync_all()
@@ -87,17 +88,16 @@ def run_test(self):
         # Get balance on node 0
         bal = self.nodes[0].z_gettotalbalance()
         print("Balance before zsend, after shielding 10: ", bal)
-        assert_equal(Decimal(bal["private"]), Decimal('10.0') - LEGACY_DEFAULT_FEE)
+        assert_equal(Decimal(bal["private"]), Decimal('10.0') - coinbase_fee)
 
         print("Splitting network...")
         self.split_network()
 
         # Create transactions
         blockheight = self.nodes[0].getblockchaininfo()['blocks']
-        zsendamount = Decimal('1.0') - conventional_fee(2)
-        recipients = []
-        recipients.append({"address": z_bob, "amount": zsendamount})
-        myopid = self.nodes[0].z_sendmany(z_alice, recipients, 1)
+        fee = conventional_fee(2)
+        recipients = [{"address": z_bob, "amount": Decimal('1.0') - fee}]
+        myopid = self.nodes[0].z_sendmany(z_alice, recipients, 1, fee)
         persist_shielded = wait_and_assert_operationid_status(self.nodes[0], myopid)
         persist_transparent = self.nodes[0].sendtoaddress(bob, 0.01)
         # Verify transparent transaction is version 4 intended for Sapling branch
@@ -196,9 +196,10 @@ def run_test(self):
 
         print("\nBlockheight advances to greater than expiry block height. After reorg, txs should expire from mempool")
         print("Balance before expire_shielded is sent: ", self.nodes[0].z_gettotalbalance())
-        myopid = self.nodes[0].z_sendmany(z_alice, recipients, 1)
+        fee = conventional_fee(2)
+        myopid = self.nodes[0].z_sendmany(z_alice, recipients, 1, fee)
         expire_shielded = wait_and_assert_operationid_status(self.nodes[0], myopid)
-        expire_transparent = self.nodes[0].sendtoaddress(bob, 0.01)
+        expire_transparent = self.nodes[0].sendtoaddress(bob, Decimal('0.01'))
         print("Blockheight node 0 at expire_transparent creation:", self.nodes[0].getblockchaininfo()['blocks'])
         print("Blockheight node 2 at expire_shielded creation:", self.nodes[2].getblockchaininfo()['blocks'])
         print("Expiryheight of expire_transparent:", self.nodes[0].getrawtransaction(expire_transparent, 1)['expiryheight'])
@@ -220,7 +221,7 @@ def run_test(self):
         print("Ensure balance of node 0 is correct")
         bal = self.nodes[0].z_gettotalbalance()
         print("Balance after expire_shielded has expired: ", bal)
-        assert_equal(Decimal(bal["private"]), Decimal('8.0') - LEGACY_DEFAULT_FEE)
+        assert_equal(Decimal(bal["private"]), Decimal('8.0') - coinbase_fee)
 
         print("Splitting network...")
         self.split_network()
@@ -229,7 +230,7 @@ def run_test(self):
         print("Balance before expire_shielded is sent: ", self.nodes[0].z_gettotalbalance())
         myopid = self.nodes[0].z_sendmany(z_alice, recipients, 1)
         expire_shielded = wait_and_assert_operationid_status(self.nodes[0], myopid)
-        expire_transparent = self.nodes[0].sendtoaddress(bob, 0.01)
+        expire_transparent = self.nodes[0].sendtoaddress(bob, Decimal('0.01'))
         print("Blockheight node 0 at expire_transparent creation:", self.nodes[0].getblockchaininfo()['blocks'])
         print("Blockheight node 2 at expire_shielded creation:", self.nodes[2].getblockchaininfo()['blocks'])
         print("Expiryheight of expire_transparent:", self.nodes[0].getrawtransaction(expire_transparent, 1)['expiryheight'])
diff --git a/qa/rpc-tests/mergetoaddress_helper.py b/qa/rpc-tests/mergetoaddress_helper.py
index 19d28b14c75..8c23093789b 100755
--- a/qa/rpc-tests/mergetoaddress_helper.py
+++ b/qa/rpc-tests/mergetoaddress_helper.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright (c) 2018 The Zcash developers
+# Copyright (c) 2018-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
@@ -10,8 +10,8 @@
 from test_framework.authproxy import JSONRPCException
 from test_framework.util import assert_equal, connect_nodes_bi, fail, \
     initialize_chain_clean, start_node, \
-    wait_and_assert_operationid_status, LEGACY_DEFAULT_FEE
-from test_framework.zip317 import conventional_fee
+    wait_and_assert_operationid_status
+from test_framework.zip317 import conventional_fee, ZIP_317_FEE
 
 from decimal import Decimal
 
@@ -39,7 +39,6 @@ def setup_chain(self, test):
 
     def setup_network(self, test, additional_args=[]):
         args = [
-            '-minrelaytxfee=0',
             '-debug=zrpcunsafe',
             '-allowdeprecated=getnewaddress',
             '-allowdeprecated=z_getnewaddress',
@@ -86,26 +85,36 @@ def generate_and_check(node, expected_transactions):
 
         # Shield the coinbase
         myzaddr = self.new_address(test, 0)
-        result = test.nodes[0].z_shieldcoinbase("*", myzaddr, 0, None, None, 'AllowLinkingAccountAddresses')
+        fee = conventional_fee(7)
+        result = test.nodes[0].z_shieldcoinbase("*", myzaddr, fee, None, None, 'AllowLinkingAccountAddresses')
         wait_and_assert_operationid_status(test.nodes[0], result['opid'])
         test.sync_all()
         generate_and_check(test.nodes[1], 2)
         test.sync_all()
+        expected_myzaddr = Decimal('50') - fee
+
+        assert_equal(test.nodes[0].z_getbalance(myzaddr), expected_myzaddr)
 
         # Prepare some UTXOs and notes for merging
         mytaddr = test.nodes[0].getnewaddress()
         mytaddr2 = test.nodes[0].getnewaddress()
         mytaddr3 = test.nodes[0].getnewaddress()
+        fee = conventional_fee(6)
         result = test.nodes[0].z_sendmany(myzaddr, [
             {'address': do_not_shield_taddr, 'amount': Decimal('10')},
             {'address': mytaddr, 'amount': Decimal('10')},
             {'address': mytaddr2, 'amount': Decimal('10')},
             {'address': mytaddr3, 'amount': Decimal('10')},
-            ], 1, 0, 'AllowRevealedRecipients')
+            ], 1, fee, 'AllowRevealedRecipients')
         wait_and_assert_operationid_status(test.nodes[0], result)
         test.sync_all()
         generate_and_check(test.nodes[1], 2)
         test.sync_all()
+        expected_myzaddr -= Decimal('40') + fee
+        expected_mytaddr = Decimal('10')
+
+        assert_equal(test.nodes[0].z_getbalance(mytaddr), expected_mytaddr)
+        assert_equal(test.nodes[0].z_getbalance(myzaddr), expected_myzaddr)
 
         # Merging will fail because from arguments need to be in an array
         assert_mergetoaddress_exception(
@@ -136,22 +145,22 @@ def generate_and_check(node, expected_transactions):
         # Merging will fail because transparent limit parameter must be at least 0
         assert_mergetoaddress_exception(
             "Limit on maximum number of UTXOs cannot be negative",
-            lambda: test.nodes[0].z_mergetoaddress(self.any_zaddr_or_utxo, myzaddr, Decimal('0.001'), -1))
+            lambda: test.nodes[0].z_mergetoaddress(self.any_zaddr_or_utxo, myzaddr, ZIP_317_FEE, -1))
 
         # Merging will fail because transparent limit parameter is absurdly large
         assert_mergetoaddress_exception(
             "JSON integer out of range",
-            lambda: test.nodes[0].z_mergetoaddress(self.any_zaddr_or_utxo, myzaddr, Decimal('0.001'), 99999999999999))
+            lambda: test.nodes[0].z_mergetoaddress(self.any_zaddr_or_utxo, myzaddr, ZIP_317_FEE, 99999999999999))
 
         # Merging will fail because shielded limit parameter must be at least 0
         assert_mergetoaddress_exception(
             "Limit on maximum number of notes cannot be negative",
-            lambda: test.nodes[0].z_mergetoaddress(self.any_zaddr_or_utxo, myzaddr, Decimal('0.001'), 50, -1))
+            lambda: test.nodes[0].z_mergetoaddress(self.any_zaddr_or_utxo, myzaddr, ZIP_317_FEE, 50, -1))
 
         # Merging will fail because shielded limit parameter is absurdly large
         assert_mergetoaddress_exception(
             "JSON integer out of range",
-            lambda: test.nodes[0].z_mergetoaddress(self.any_zaddr_or_utxo, myzaddr, Decimal('0.001'), 50, 99999999999999))
+            lambda: test.nodes[0].z_mergetoaddress(self.any_zaddr_or_utxo, myzaddr, ZIP_317_FEE, 50, 99999999999999))
 
         # Merging will fail for this specific case where it would spend a fee and do nothing
         assert_mergetoaddress_exception(
@@ -164,22 +173,27 @@ def generate_and_check(node, expected_transactions):
             lambda: test.nodes[0].z_mergetoaddress(["ANY_SPROUT", "ANY_SAPLING"], mytaddr))
 
         # Merge UTXOs from node 0 of value 30, default fee
-        result = test.nodes[0].z_mergetoaddress([mytaddr, mytaddr2, mytaddr3], myzaddr, None, None, None, None, 'AllowLinkingAccountAddresses')
+        fee = conventional_fee(5)
+        result = test.nodes[0].z_mergetoaddress([mytaddr, mytaddr2, mytaddr3], myzaddr, fee, None, None, None, 'AllowLinkingAccountAddresses')
         wait_and_assert_operationid_status(test.nodes[0], result['opid'])
         test.sync_all()
         generate_and_check(test.nodes[1], 2)
         test.sync_all()
+        expected_myzaddr += Decimal('30') - fee
+        expected_mytaddr = 0
 
         # Confirm balances and that do_not_shield_taddr containing funds of 10 was left alone
         assert_equal(test.nodes[0].getbalance(), Decimal('10'))
         assert_equal(test.nodes[0].z_getbalance(do_not_shield_taddr), Decimal('10'))
-        assert_equal(test.nodes[0].z_getbalance(myzaddr), Decimal('40') - conventional_fee(5))
+        assert_equal(test.nodes[0].z_getbalance(mytaddr), expected_mytaddr)
+        assert_equal(test.nodes[0].z_getbalance(myzaddr), expected_myzaddr)
         assert_equal(test.nodes[1].getbalance(), Decimal('40'))
         assert_equal(test.nodes[2].getbalance(), Decimal('30'))
 
         # Shield all notes to another z-addr
         myzaddr2 = self.new_address(test, 0)
-        result = test.nodes[0].z_mergetoaddress(self.any_zaddr, myzaddr2, 0)
+        fee = conventional_fee(2)
+        result = test.nodes[0].z_mergetoaddress(self.any_zaddr, myzaddr2, fee)
         assert_equal(result["mergingUTXOs"], 0)
         assert_equal(result["remainingUTXOs"], 0)
         assert_equal(result["mergingNotes"], 2)
@@ -188,54 +202,65 @@ def generate_and_check(node, expected_transactions):
         test.sync_all()
         generate_and_check(test.nodes[1], 2)
         test.sync_all()
+        expected_myzaddr2 = expected_myzaddr - fee
+        expected_myzaddr = 0
 
-        assert_equal(test.nodes[0].z_getbalance(myzaddr), Decimal('0'))
-        assert_equal(test.nodes[0].z_getbalance(myzaddr2), Decimal('40') - conventional_fee(5))
+        assert_equal(test.nodes[0].z_getbalance(myzaddr), expected_myzaddr)
+        assert_equal(test.nodes[0].z_getbalance(myzaddr2), expected_myzaddr2)
 
-        # Shield coinbase UTXOs from any node 2 taddr, and set fee to 0
-        result = test.nodes[2].z_shieldcoinbase("*", myzaddr, 0, None, None, 'AllowLinkingAccountAddresses')
+        # Shield coinbase UTXOs from any node 2 taddr
+        fee = conventional_fee(5)
+        result = test.nodes[2].z_shieldcoinbase("*", myzaddr, fee, None, None, 'AllowLinkingAccountAddresses')
         wait_and_assert_operationid_status(test.nodes[2], result['opid'])
         test.sync_all()
         generate_and_check(test.nodes[1], 2)
         test.sync_all()
+        expected_myzaddr += Decimal('30') - fee
 
         assert_equal(test.nodes[0].getbalance(), Decimal('10'))
-        assert_equal(test.nodes[0].z_getbalance(myzaddr), Decimal('30'))
-        assert_equal(test.nodes[0].z_getbalance(myzaddr2), Decimal('40') - conventional_fee(5))
+        assert_equal(test.nodes[0].z_getbalance(myzaddr), expected_myzaddr)
+        assert_equal(test.nodes[0].z_getbalance(myzaddr2), expected_myzaddr2)
         assert_equal(test.nodes[1].getbalance(), Decimal('60'))
         assert_equal(test.nodes[2].getbalance(), Decimal('0'))
 
-        # Merge all notes from node 0 into a node 0 taddr, and set fee to 0
-        result = test.nodes[0].z_mergetoaddress(self.any_zaddr, mytaddr, 0, None, None, None, 'AllowRevealedRecipients')
+        # Merge all notes from node 0 into a node 0 taddr
+        fee = conventional_fee(3)
+        result = test.nodes[0].z_mergetoaddress(self.any_zaddr, mytaddr, fee, None, None, None, 'AllowRevealedRecipients')
         wait_and_assert_operationid_status(test.nodes[0], result['opid'])
         test.sync_all()
         generate_and_check(test.nodes[1], 2)
         test.sync_all()
+        expected_mytaddr += expected_myzaddr + expected_myzaddr2 - fee
+        expected_myzaddr = 0
+        expected_myzaddr2 = 0
 
-        assert_equal(test.nodes[0].getbalance(), Decimal('80') - conventional_fee(5))
+        assert_equal(test.nodes[0].getbalance(), Decimal('10') + expected_mytaddr)
         assert_equal(test.nodes[0].z_getbalance(do_not_shield_taddr), Decimal('10'))
-        assert_equal(test.nodes[0].z_getbalance(mytaddr), Decimal('70') - conventional_fee(5))
-        assert_equal(test.nodes[0].z_getbalance(myzaddr), Decimal('0'))
-        assert_equal(test.nodes[0].z_getbalance(myzaddr2), Decimal('0'))
+        assert_equal(test.nodes[0].z_getbalance(mytaddr), expected_mytaddr)
+        assert_equal(test.nodes[0].z_getbalance(myzaddr), expected_myzaddr)
+        assert_equal(test.nodes[0].z_getbalance(myzaddr2), expected_myzaddr2)
         assert_equal(test.nodes[1].getbalance(), Decimal('70'))
         assert_equal(test.nodes[2].getbalance(), Decimal('0'))
 
-        # Merge all node 0 UTXOs together into a node 1 taddr, and set fee to 0
+        # Merge all node 0 UTXOs together into a node 1 taddr
         test.nodes[1].getnewaddress()  # Ensure we have an empty address
         n1taddr = test.nodes[1].getnewaddress()
-        result = test.nodes[0].z_mergetoaddress(["ANY_TADDR"], n1taddr, 0, None, None, None, 'NoPrivacy')
+        fee = conventional_fee(4)
+        result = test.nodes[0].z_mergetoaddress(["ANY_TADDR"], n1taddr, fee, None, None, None, 'NoPrivacy')
         wait_and_assert_operationid_status(test.nodes[0], result['opid'])
         test.sync_all()
         generate_and_check(test.nodes[1], 2)
         test.sync_all()
         assert_equal(0, len(test.nodes[0].z_listunspent(0)))
+        expected_n1taddr = Decimal('10') + expected_mytaddr - fee
+        expected_mytaddr = 0
 
         assert_equal(test.nodes[0].getbalance(), Decimal('0'))
         assert_equal(test.nodes[0].z_getbalance(do_not_shield_taddr), Decimal('0'))
-        assert_equal(test.nodes[0].z_getbalance(mytaddr), Decimal('0'))
-        assert_equal(test.nodes[0].z_getbalance(myzaddr), Decimal('0'))
-        assert_equal(test.nodes[1].getbalance(), Decimal('160') - conventional_fee(5))
-        assert_equal(test.nodes[1].z_getbalance(n1taddr), Decimal('80') - conventional_fee(5))
+        assert_equal(test.nodes[0].z_getbalance(mytaddr), expected_mytaddr)
+        assert_equal(test.nodes[0].z_getbalance(myzaddr), expected_myzaddr)
+        assert_equal(test.nodes[1].getbalance(), Decimal('80') + expected_n1taddr)
+        assert_equal(test.nodes[1].z_getbalance(n1taddr), expected_n1taddr)
         assert_equal(test.nodes[2].getbalance(), Decimal('0'))
 
         # Generate 5 regular UTXOs on node 0, and 20 regular UTXOs on node 2.
@@ -251,7 +276,7 @@ def generate_and_check(node, expected_transactions):
         test.sync_all()
 
         # This z_mergetoaddress and the one below result in two notes in myzaddr.
-        result = test.nodes[0].z_mergetoaddress([mytaddr], myzaddr, LEGACY_DEFAULT_FEE, None, None, None, 'AllowRevealedSenders')
+        result = test.nodes[0].z_mergetoaddress([mytaddr], myzaddr, ZIP_317_FEE, None, None, None, 'AllowRevealedSenders')
         assert_equal(result["mergingUTXOs"], 5)
         assert_equal(result["remainingUTXOs"], 0)
         assert_equal(result["mergingNotes"], 0)
@@ -259,7 +284,7 @@ def generate_and_check(node, expected_transactions):
         wait_and_assert_operationid_status(test.nodes[0], result['opid'])
 
         # Verify maximum number of UTXOs is not limited when the limit parameter is set to 0.
-        result = test.nodes[2].z_mergetoaddress([n2taddr], myzaddr, LEGACY_DEFAULT_FEE, 0, None, None, 'AllowRevealedSenders')
+        result = test.nodes[2].z_mergetoaddress([n2taddr], myzaddr, ZIP_317_FEE, 0, None, None, 'AllowRevealedSenders')
         assert_equal(result["mergingUTXOs"], 20)
         assert_equal(result["remainingUTXOs"], 0)
         assert_equal(result["mergingNotes"], 0)
@@ -276,7 +301,7 @@ def generate_and_check(node, expected_transactions):
             test.nodes[1].sendtoaddress(mytaddr, 1)
         generate_and_check(test.nodes[1], 101)
         test.sync_all()
-        result = test.nodes[0].z_mergetoaddress([mytaddr], myzaddr, conventional_fee(52), None, None, None, 'AllowRevealedSenders')
+        result = test.nodes[0].z_mergetoaddress([mytaddr], myzaddr, ZIP_317_FEE, None, None, None, 'AllowRevealedSenders')
         assert_equal(result["mergingUTXOs"], 50)
         assert_equal(result["remainingUTXOs"], 50)
         assert_equal(result["mergingNotes"], 0)
@@ -288,7 +313,7 @@ def generate_and_check(node, expected_transactions):
         assert_equal(3, len(test.nodes[0].z_listunspent(0)))
 
         # Verify maximum number of UTXOs which node 0 can shield can be set by the limit parameter
-        result = test.nodes[0].z_mergetoaddress([mytaddr], myzaddr, conventional_fee(35), 33, None, None, 'AllowRevealedSenders')
+        result = test.nodes[0].z_mergetoaddress([mytaddr], myzaddr, ZIP_317_FEE, 33, None, None, 'AllowRevealedSenders')
         assert_equal(result["mergingUTXOs"], 33)
         assert_equal(result["remainingUTXOs"], 17)
         assert_equal(result["mergingNotes"], 0)
@@ -303,7 +328,7 @@ def generate_and_check(node, expected_transactions):
         # NB: We can’t yet merge from UAs, so ensure we’re not before running these cases
         if (myzaddr[0] != 'u'):
             # Also check that we can set off a second merge before the first one is complete
-            result1 = test.nodes[0].z_mergetoaddress([myzaddr], myzaddr, LEGACY_DEFAULT_FEE, 50, 2)
+            result1 = test.nodes[0].z_mergetoaddress([myzaddr], myzaddr, ZIP_317_FEE, 50, 2)
 
             # First merge should select from all notes
             assert_equal(result1["mergingUTXOs"], 0)
@@ -313,7 +338,7 @@ def generate_and_check(node, expected_transactions):
             assert_equal(result1["remainingNotes"], 2)
 
             # Second merge should ignore locked notes
-            result2 = test.nodes[0].z_mergetoaddress([myzaddr], myzaddr, LEGACY_DEFAULT_FEE, 50, 2)
+            result2 = test.nodes[0].z_mergetoaddress([myzaddr], myzaddr, ZIP_317_FEE, 50, 2)
             assert_equal(result2["mergingUTXOs"], 0)
             assert_equal(result2["remainingUTXOs"], 0)
             assert_equal(result2["mergingNotes"], 2)
@@ -326,7 +351,7 @@ def generate_and_check(node, expected_transactions):
             test.sync_all()
 
             # Shield both UTXOs and notes to a z-addr
-            result = test.nodes[0].z_mergetoaddress(self.any_zaddr_or_utxo, myzaddr, LEGACY_DEFAULT_FEE, 10, 2, None, 'AllowRevealedSenders')
+            result = test.nodes[0].z_mergetoaddress(self.any_zaddr_or_utxo, myzaddr, ZIP_317_FEE, 10, 2, None, 'AllowRevealedSenders')
             assert_equal(result["mergingUTXOs"], 10)
             assert_equal(result["remainingUTXOs"], 7)
             assert_equal(result["mergingNotes"], 2)
@@ -334,7 +359,7 @@ def generate_and_check(node, expected_transactions):
             wait_and_assert_operationid_status(test.nodes[0], result['opid'])
         else:
             # Shield both UTXOs and notes to a z-addr
-            result = test.nodes[0].z_mergetoaddress(self.any_zaddr_or_utxo, myzaddr, LEGACY_DEFAULT_FEE, 10, 2, None, 'AllowRevealedSenders')
+            result = test.nodes[0].z_mergetoaddress(self.any_zaddr_or_utxo, myzaddr, ZIP_317_FEE, 10, 2, None, 'AllowRevealedSenders')
             assert_equal(result["mergingUTXOs"], 10)
             assert_equal(result["remainingUTXOs"], 7)
             assert_equal(result["mergingNotes"], 2)
diff --git a/qa/rpc-tests/mergetoaddress_mixednotes.py b/qa/rpc-tests/mergetoaddress_mixednotes.py
index 968d5b5fb1f..524a85dc686 100755
--- a/qa/rpc-tests/mergetoaddress_mixednotes.py
+++ b/qa/rpc-tests/mergetoaddress_mixednotes.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright (c) 2019 The Zcash developers
+# Copyright (c) 2019-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
@@ -7,6 +7,7 @@
 from test_framework.test_framework import BitcoinTestFramework
 from test_framework.util import assert_equal, get_coinbase_address, \
     start_nodes, wait_and_assert_operationid_status
+from test_framework.zip317 import conventional_fee
 from mergetoaddress_helper import assert_mergetoaddress_exception
 
 
@@ -18,7 +19,6 @@ def __init__(self):
     def setup_nodes(self):
         self.num_nodes = 4
         return start_nodes(self.num_nodes, self.options.tmpdir, extra_args=[[
-            '-minrelaytxfee=0',
             '-anchorconfirmations=1',
             '-allowdeprecated=getnewaddress',
             '-allowdeprecated=legacy_privacy',
@@ -38,10 +38,11 @@ def run_test(self):
         assert_equal(self.nodes[0].z_getbalance(saplingAddr), Decimal('0'))
         assert_equal(Decimal(self.nodes[1].z_gettotalbalance()["transparent"]), Decimal('200'))
         # Make sure we cannot use "ANY_SPROUT" and "ANY_SAPLING" even if we only have Sprout Notes
+        fee = conventional_fee(3)
         assert_mergetoaddress_exception(
             "Cannot send from both Sprout and Sapling addresses using z_mergetoaddress",
             lambda: self.nodes[0].z_mergetoaddress(["ANY_SPROUT", "ANY_SAPLING"], t_addr))
-        opid = self.nodes[0].z_sendmany(coinbase_addr, [{"address": saplingAddr, "amount": Decimal('10')}], 1, 0, 'AllowRevealedSenders')
+        opid = self.nodes[0].z_sendmany(coinbase_addr, [{"address": saplingAddr, "amount": Decimal('10') - fee}], 1, fee, 'AllowRevealedSenders')
         wait_and_assert_operationid_status(self.nodes[0], opid)
         self.nodes[0].generate(1)
         self.sync_all()
@@ -49,28 +50,28 @@ def run_test(self):
         assert_equal(Decimal(self.nodes[1].z_gettotalbalance()["transparent"]), Decimal('200'))
 
         # Merge Sprout -> taddr
-        result = self.nodes[0].z_mergetoaddress(["ANY_SPROUT"], t_addr, 0)
+        result = self.nodes[0].z_mergetoaddress(["ANY_SPROUT"], t_addr)
         wait_and_assert_operationid_status(self.nodes[0], result['opid'])
         self.nodes[0].generate(1)
         self.sync_all()
 
         assert_equal(self.nodes[0].z_getbalance(sproutAddr), Decimal('0'))
-        assert_equal(self.nodes[0].z_getbalance(saplingAddr), Decimal('10'))
-        assert_equal(Decimal(self.nodes[1].z_gettotalbalance()["transparent"]), Decimal('250'))
+        assert_equal(self.nodes[0].z_getbalance(saplingAddr), Decimal('10') - fee)
+        assert_equal(Decimal(self.nodes[1].z_gettotalbalance()["transparent"]), Decimal('250') - fee)
 
         # Make sure we cannot use "ANY_SPROUT" and "ANY_SAPLING" even if we only have Sapling Notes
         assert_mergetoaddress_exception(
             "Cannot send from both Sprout and Sapling addresses using z_mergetoaddress",
-            lambda: self.nodes[0].z_mergetoaddress(["ANY_SPROUT", "ANY_SAPLING"], t_addr))
+            lambda: self.nodes[0].z_mergetoaddress(["ANY_SPROUT", "ANY_SAPLING"], t_addr, fee))
         # Merge Sapling -> taddr
-        result = self.nodes[0].z_mergetoaddress(["ANY_SAPLING"], t_addr, 0)
+        result = self.nodes[0].z_mergetoaddress(["ANY_SAPLING"], t_addr, fee)
         wait_and_assert_operationid_status(self.nodes[0], result['opid'])
         self.nodes[0].generate(1)
         self.sync_all()
 
         assert_equal(self.nodes[0].z_getbalance(sproutAddr), Decimal('0'))
         assert_equal(self.nodes[0].z_getbalance(saplingAddr), Decimal('0'))
-        assert_equal(Decimal(self.nodes[1].z_gettotalbalance()["transparent"]), Decimal('260'))
+        assert_equal(Decimal(self.nodes[1].z_gettotalbalance()["transparent"]), Decimal('260') - 3*fee)
 
 
 if __name__ == '__main__':
diff --git a/qa/rpc-tests/merkle_blocks.py b/qa/rpc-tests/merkle_blocks.py
index 9778b7d589d..c8f66e1b79d 100755
--- a/qa/rpc-tests/merkle_blocks.py
+++ b/qa/rpc-tests/merkle_blocks.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 # Copyright (c) 2014-2016 The Bitcoin Core developers
-# Copyright (c) 2016-2022 The Zcash developers
+# Copyright (c) 2016-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
@@ -11,8 +11,8 @@
 import string
 from test_framework.test_framework import BitcoinTestFramework
 from test_framework.authproxy import JSONRPCException
-from test_framework.util import assert_equal, assert_raises, \
-    start_node, connect_nodes
+from test_framework.util import assert_equal, assert_raises_message, start_node, connect_nodes
+from test_framework.zip317 import conventional_fee
 
 
 class MerkleBlockTest(BitcoinTestFramework):
@@ -24,7 +24,6 @@ def __init__(self):
 
     def setup_network(self):
         base_args = [
-            '-minrelaytxfee=0',
             '-debug',
             '-allowdeprecated=getnewaddress',
         ]
@@ -53,11 +52,11 @@ def run_test(self):
         assert_equal(self.nodes[2].getbalance(), 0)
 
         node0utxos = self.nodes[0].listunspent(1)
-        tx1 = self.nodes[0].createrawtransaction([node0utxos.pop()], {self.nodes[1].getnewaddress(): 10})
+        tx1 = self.nodes[0].createrawtransaction([node0utxos.pop()], {self.nodes[1].getnewaddress(): 10 - conventional_fee(1)})
         txid1 = self.nodes[0].sendrawtransaction(self.nodes[0].signrawtransaction(tx1)["hex"])
-        tx2 = self.nodes[0].createrawtransaction([node0utxos.pop()], {self.nodes[1].getnewaddress(): 10})
+        tx2 = self.nodes[0].createrawtransaction([node0utxos.pop()], {self.nodes[1].getnewaddress(): 10 - conventional_fee(1)})
         txid2 = self.nodes[0].sendrawtransaction(self.nodes[0].signrawtransaction(tx2)["hex"])
-        assert_raises(JSONRPCException, self.nodes[0].gettxoutproof, [txid1])
+        assert_raises_message(JSONRPCException, "Transaction not yet in block", self.nodes[0].gettxoutproof, [txid1])
 
         self.nodes[0].generate(1)
         blockhash = self.nodes[0].getblockhash(chain_height + 1)
@@ -73,7 +72,7 @@ def run_test(self):
         assert_equal(self.nodes[2].verifytxoutproof(self.nodes[2].gettxoutproof([txid1, txid2], blockhash)), txlist)
 
         txin_spent = self.nodes[1].listunspent(1).pop()
-        tx3 = self.nodes[1].createrawtransaction([txin_spent], {self.nodes[0].getnewaddress(): 10})
+        tx3 = self.nodes[1].createrawtransaction([txin_spent], {self.nodes[0].getnewaddress(): 10 - 2*conventional_fee(1)})
         self.nodes[0].sendrawtransaction(self.nodes[1].signrawtransaction(tx3)["hex"])
         self.nodes[0].generate(1)
         self.sync_all()
@@ -82,7 +81,7 @@ def run_test(self):
         txid_unspent = txid1 if txin_spent["txid"] != txid1 else txid2
 
         # We can't find the block from a fully-spent tx
-        assert_raises(JSONRPCException, self.nodes[2].gettxoutproof, [txid_spent])
+        assert_raises_message(JSONRPCException, "Transaction not yet in block", self.nodes[2].gettxoutproof, [txid_spent])
         # ...but we can if we specify the block
         assert_equal(self.nodes[2].verifytxoutproof(self.nodes[2].gettxoutproof([txid_spent], blockhash)), [txid_spent])
         # ...or if the first tx is not fully-spent
@@ -104,15 +103,15 @@ def run_test(self):
 
         # Test getblock heights including negatives relative to the head
         assert_equal(self.nodes[0].getblock("0")["height"], 0)
-        assert_raises(JSONRPCException, self.nodes[0].getblock, ["108"])
+        assert_raises_message(JSONRPCException, "Block height out of range", self.nodes[0].getblock, "108")
         assert_equal(self.nodes[0].getblock("107")["height"], 107)
         assert_equal(self.nodes[0].getblock("-1")["height"], 107)
         assert_equal(self.nodes[0].getblock("-2")["height"], 106)
         assert_equal(self.nodes[0].getblock("-20")["height"], 88)
         assert_equal(self.nodes[0].getblock("-107")["height"], 1)
         assert_equal(self.nodes[0].getblock("-108")["height"], 0)
-        assert_raises(JSONRPCException, self.nodes[0].getblock, ["-109"])
-        assert_raises(JSONRPCException, self.nodes[0].getblock, ["-0"])
+        assert_raises_message(JSONRPCException, "Block height out of range", self.nodes[0].getblock, "-109")
+        assert_raises_message(JSONRPCException, "Invalid block height parameter", self.nodes[0].getblock, "-0")
 
         # Test getblockhash negative heights
         assert_equal(self.nodes[0].getblockhash(-1), self.nodes[0].getblockhash(107))
diff --git a/qa/rpc-tests/mining_shielded_coinbase.py b/qa/rpc-tests/mining_shielded_coinbase.py
index 118ec4f9fe6..62c568d8668 100755
--- a/qa/rpc-tests/mining_shielded_coinbase.py
+++ b/qa/rpc-tests/mining_shielded_coinbase.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright (c) 2019 The Zcash developers
+# Copyright (c) 2019-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
@@ -20,6 +20,7 @@
     wait_and_assert_operationid_status,
     check_node_log,
 )
+from test_framework.zip317 import conventional_fee
 
 class ShieldCoinbaseTest (BitcoinTestFramework):
 
@@ -30,7 +31,6 @@ def __init__(self):
 
     def start_node_with(self, index, extra_args=[]):
         args = [
-            '-minrelaytxfee=0',
             nuparams(BLOSSOM_BRANCH_ID, 1),
             nuparams(HEARTWOOD_BRANCH_ID, 10),
             nuparams(CANOPY_BRANCH_ID, 20),
@@ -96,16 +96,16 @@ def run_test (self):
         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')
+        assert_equal(Decimal(self.nodes[0].getbalance()), Decimal('0'))
+        assert_equal(Decimal(self.nodes[0].z_gettotalbalance()['transparent']), Decimal('0'))
+        assert_equal(Decimal(self.nodes[0].z_gettotalbalance()['private']), Decimal('0'))
+        assert_equal(Decimal(self.nodes[0].z_gettotalbalance()['total']), Decimal('0'))
 
         # 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')
+        assert_equal(Decimal(self.nodes[1].z_getbalance(node1_zaddr, 0)), Decimal('5'))
+        assert_equal(Decimal(self.nodes[1].z_getbalance(node1_zaddr)), Decimal('5'))
+        assert_equal(Decimal(self.nodes[1].z_gettotalbalance()['private']), Decimal('5'))
+        assert_equal(Decimal(self.nodes[1].z_gettotalbalance()['total']), Decimal('5'))
 
         # Send from Sapling coinbase to Sapling address and transparent address
         # (to check that a non-empty vout is allowed when spending shielded
@@ -113,18 +113,20 @@ def run_test (self):
         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, 'AllowRevealedRecipients')
+        fee = conventional_fee(3)
+        recipients = [
+            {"address": node0_zaddr, "amount": Decimal('2')},
+            {"address": node0_taddr, "amount": Decimal('2')},
+        ]
+        myopid = self.nodes[1].z_sendmany(node1_zaddr, recipients, 1, fee, 'AllowRevealedRecipients')
         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)
+        assert_equal(Decimal(self.nodes[0].z_getbalance(node0_zaddr)), Decimal('2'))
+        assert_equal(Decimal(self.nodes[0].z_getbalance(node0_taddr)), Decimal('2'))
+        assert_equal(Decimal(self.nodes[1].z_getbalance(node1_zaddr)), Decimal('1') - fee)
 
         # Generate a Unified Address for node 1
         self.nodes[1].z_getnewaccount()
diff --git a/qa/rpc-tests/orchard_reorg.py b/qa/rpc-tests/orchard_reorg.py
index 420118393ea..cbe32b229e0 100755
--- a/qa/rpc-tests/orchard_reorg.py
+++ b/qa/rpc-tests/orchard_reorg.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright (c) 2022 The Zcash developers
+# Copyright (c) 2022-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
@@ -19,11 +19,10 @@
     start_nodes,
     wait_and_assert_operationid_status,
 )
+from test_framework.zip317 import ZIP_317_FEE
 
 from finalsaplingroot import ORCHARD_TREE_EMPTY_ROOT
 
-from decimal import Decimal
-
 class OrchardReorgTest(BitcoinTestFramework):
     def __init__(self):
         super().__init__()
@@ -32,7 +31,6 @@ def __init__(self):
 
     def setup_nodes(self):
         return start_nodes(self.num_nodes, self.options.tmpdir, extra_args=[[
-            '-minrelaytxfee=0',
             nuparams(BLOSSOM_BRANCH_ID, 1),
             nuparams(HEARTWOOD_BRANCH_ID, 5),
             nuparams(CANOPY_BRANCH_ID, 5),
@@ -67,9 +65,8 @@ def run_test(self):
         )
 
         # Create an Orchard note.
-        recipients = [{'address': ua, 'amount': Decimal('12.5')}]
-        opid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, 0, 'AllowRevealedSenders')
-        wait_and_assert_operationid_status(self.nodes[0], opid)
+        res = self.nodes[0].z_shieldcoinbase(get_coinbase_address(self.nodes[0]), ua, ZIP_317_FEE, None, None, 'AllowRevealedSenders')
+        wait_and_assert_operationid_status(self.nodes[0], res['opid'])
 
         # After mining a block, finalorchardroot should have changed.
         self.sync_all()
@@ -92,9 +89,8 @@ def run_test(self):
         self.split_network()
 
         # Create another Orchard note on node 0.
-        recipients = [{'address': ua, 'amount': Decimal('12.5')}]
-        opid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, 0, 'AllowRevealedSenders')
-        wait_and_assert_operationid_status(self.nodes[0], opid)
+        res = self.nodes[0].z_shieldcoinbase(get_coinbase_address(self.nodes[0]), ua, ZIP_317_FEE, None, None, 'AllowRevealedSenders')
+        wait_and_assert_operationid_status(self.nodes[0], res['opid'])
 
         # Mine two blocks on node 0.
         print("Mining 2 blocks on node 0")
diff --git a/qa/rpc-tests/p2p_txexpiringsoon.py b/qa/rpc-tests/p2p_txexpiringsoon.py
index c0c127e5d51..22d0726b8d9 100755
--- a/qa/rpc-tests/p2p_txexpiringsoon.py
+++ b/qa/rpc-tests/p2p_txexpiringsoon.py
@@ -1,14 +1,18 @@
 #!/usr/bin/env python3
-# Copyright (c) 2018 The Zcash developers
+# Copyright (c) 2018-2024 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.mininode import NodeConn, NetworkThread, CInv, \
     msg_mempool, msg_getdata, msg_tx, mininode_lock, SAPLING_PROTO_VERSION
 from test_framework.test_framework import BitcoinTestFramework
 from test_framework.util import assert_equal, connect_nodes_bi, fail, \
     p2p_port, start_nodes, sync_blocks, sync_mempools
+from test_framework.zip317 import conventional_fee
+
 from tx_expiry_helper import TestNode, create_transaction
 
 
@@ -21,7 +25,6 @@ def __init__(self):
 
     def setup_network(self):
         self.nodes = start_nodes(self.num_nodes, self.options.tmpdir, extra_args=[[
-            '-minrelaytxfee=0',
             '-allowdeprecated=getnewaddress',
         ]] * self.num_nodes)
         connect_nodes_bi(self.nodes, 0, 1)
@@ -31,7 +34,7 @@ def send_transaction(self, testnode, block, address, expiry_height):
         tx = create_transaction(self.nodes[0],
                                 block,
                                 address,
-                                10.0,
+                                Decimal("10.0") - conventional_fee(2),
                                 expiry_height)
         testnode.send_message(msg_tx(tx))
 
diff --git a/qa/rpc-tests/prioritisetransaction.py b/qa/rpc-tests/prioritisetransaction.py
index 41655590212..ac6022b41c2 100755
--- a/qa/rpc-tests/prioritisetransaction.py
+++ b/qa/rpc-tests/prioritisetransaction.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright (c) 2017 The Zcash developers
+# Copyright (c) 2017-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
@@ -28,10 +28,10 @@ def setup_nodes(self):
             '-debug=zrpcunsafe',
         ]
         return start_nodes(self.num_nodes, self.options.tmpdir, extra_args=[
+            args + ['-txunpaidactionlimit=51', '-blockunpaidactionlimit=50'],
+            args + ['-txunpaidactionlimit=51', '-blockunpaidactionlimit=32'],
             args + ['-txunpaidactionlimit=51'],
-            args + ['-txunpaidactionlimit=51', '-blockunpaidactionlimit=25'],
-            args + ['-txunpaidactionlimit=51'],
-            args + ['-txunpaidactionlimit=25'],
+            args + ['-txunpaidactionlimit=32'],
         ])
 
     def run_test(self):
@@ -39,12 +39,17 @@ def run_test(self):
         # but the halving interval is only 144 blocks.
 
         # For the first test the miner subsidy is 10 ZEC.
-        self.test(self.nodes[0], Decimal("10"), DEFAULT_BLOCK_UNPAID_ACTION_LIMIT)
+        self.test(self.nodes[0], Decimal("10"), 50)
         assert_equal(153, self.nodes[0].getblockcount())
 
         # For the second test the miner subsidy is 6.25 ZEC.
         # (The Founders' Reward has expired and there are no funding streams.)
-        self.test(self.nodes[1], Decimal("6.25"), 25)
+        self.test(self.nodes[1], Decimal("6.25"), 32)
+        assert_equal(288, self.nodes[0].getblockcount())
+
+        # For the third test the miner subsidy is 3.125 ZEC.
+        self.test(self.nodes[2], Decimal("3.125"), DEFAULT_BLOCK_UNPAID_ACTION_LIMIT)
+        assert_equal(392, self.nodes[0].getblockcount())
 
     def test(self, mining_node, miner_subsidy, block_unpaid_action_limit):
         print("Testing with -blockunpaidactionlimit=%d" % (block_unpaid_action_limit,))
@@ -62,43 +67,61 @@ def eventually_in_template(txid):
             return False
 
         # Make sure we have enough mature funds on mining_node.
-        blocks = 100 + block_unpaid_action_limit + 2
+        n = max(1, block_unpaid_action_limit)
+        blocks = 100 + n + 2
         print("Mining %d blocks..." % (blocks,))
         mining_node.generate(blocks)
         self.sync_all()
 
-        node2_initial_balance = self.nodes[2].getbalance()
         node3_initial_balance = self.nodes[3].getbalance()
 
         if block_unpaid_action_limit == 50:
             # Create a tx that will not be accepted to the mempool because it has
             # more unpaid actions than `-txunpaidactionlimit`.
-            amount = miner_subsidy * (block_unpaid_action_limit + 1)
+            amount = miner_subsidy * 51
             assert_equal(amount + miner_subsidy, mining_node.getbalance())
-            assert_raises_message(JSONRPCException, "tx unpaid action limit exceeded",
+            assert_raises_message(JSONRPCException,
+                    "tx unpaid action limit exceeded: 52 action(s) exceeds limit of 51",
                     mining_node.sendtoaddress,
-                    self.nodes[2].getnewaddress(), amount)
+                    self.nodes[3].getnewaddress(), amount)
 
         # Create a tx that will not be mined unless prioritised.
-        # We spend `block_unpaid_action_limit` mining rewards, ensuring that
-        # tx has exactly `block_unpaid_action_limit + 1` logical actions,
-        # because one extra input will be needed to pay the fee.
+        # We spend `n` mining rewards, ensuring that tx has exactly `n + 1`
+        # logical actions, because one extra input will be needed to pay the fee.
+        #
         # Since we've set `-paytxfee` to pay only the relay fee rate, the fee
         # will be less than the marginal fee, so these are all unpaid actions.
         # This transaction will be relayed to nodes 1 and 2 despite being over
         # the block unpaid action limit, because we set `-txunpaidactionlimit=51`
         # on nodes 0, 1, and 2.
-        amount = miner_subsidy * block_unpaid_action_limit
+        amount = miner_subsidy * n
         assert_equal(amount + miner_subsidy*2, mining_node.getbalance())
-        tx = mining_node.sendtoaddress(self.nodes[2].getnewaddress(), amount)
+        tx = mining_node.sendtoaddress(self.nodes[3].getnewaddress(), amount)
 
         mempool = mining_node.getrawmempool(True)
         assert tx in mempool, mempool
         fee_zats = int(mempool[tx]['fee'] * COIN)
         assert fee_zats < MARGINAL_FEE, fee_zats
+
         tx_verbose = mining_node.getrawtransaction(tx, 1)
-        assert_equal(block_unpaid_action_limit + 1, len(tx_verbose['vin']))
-        # TODO after #6676: assert_equal(block_unpaid_action_limit + 1, tx_verbose['unpaidActions'])
+        t_in_actions = len(tx_verbose['vin'])
+        t_out_actions = len(tx_verbose['vout'])
+        assert_equal(n + 1, t_in_actions)
+        assert_equal(2, t_out_actions)
+        assert_equal([], tx_verbose['vjoinsplit'])
+        assert_equal([], tx_verbose['vShieldedSpend'])
+        assert_equal([], tx_verbose['vShieldedOutput'])
+        if tx_verbose['version'] >= 5:
+            assert_equal([], tx_verbose['orchard']['actions'])
+        else:
+            assert('orchard' not in tx_verbose)
+
+        unpaid_actions = max(t_in_actions, t_out_actions)
+        assert_equal(unpaid_actions, n + 1)
+        # TODO after #6676: assert_equal(unpaid_actions, tx_verbose['unpaidActions'])
+
+        # Calculate the effective fee we would need to satisfy the limit.
+        required_fee = (unpaid_actions - block_unpaid_action_limit) * MARGINAL_FEE
 
         # Check that tx is not in a new block template prior to prioritisation.
         block_template = mining_node.getblocktemplate()
@@ -118,31 +141,34 @@ def sync_and_check():
             time.sleep(1)
             sync_mempools(self.nodes[3:])
 
-            # tx should not have been accepted to node3's mempool because that node has
-            # `-txunpaidactionlimit=25`, which is less than `block_unpaid_action_limit + 1`.
-            assert 25 < block_unpaid_action_limit + 1
+            # For the first two tests, tx should not be accepted to node3's
+            # mempool because that node has `-txunpaidactionlimit=32`, which is
+            # less than `unpaid_actions` in those tests. For the third test, it
+            # should be accepted, but might already have been mined.
             mempool = self.nodes[3].getrawmempool(True)
-            assert tx not in mempool, mempool
+            if 32 < unpaid_actions:
+                assert tx not in mempool, mempool
 
-        # Prioritising it on node 2 has no effect on mining_node.
+        # Prioritising it on a node other than mining_node has no effect.
+        other_node = self.nodes[0 if mining_node == self.nodes[2] else 2]
         sync_and_check()
-        priority_success = self.nodes[2].prioritisetransaction(tx, 0, MARGINAL_FEE)
+        priority_success = other_node.prioritisetransaction(tx, 0, required_fee)
         assert(priority_success)
-        mempool = self.nodes[2].getrawmempool(True)
-        assert_equal(fee_zats + MARGINAL_FEE, mempool[tx]['modifiedfee'] * COIN)
+        mempool = other_node.getrawmempool(True)
+        assert_equal(fee_zats + required_fee, mempool[tx]['modifiedfee'] * COIN)
         sync_and_check()
         send_fully_paid_transaction()
         assert_equal(eventually_in_template(tx), False)
 
         # Putting a non-zero value in the obsolete "priority" field causes an error.
         assert_raises_message(JSONRPCException, "Priority is not supported",
-                self.nodes[2].prioritisetransaction, tx, 1, 0)
+                other_node.prioritisetransaction, tx, 1, 0)
 
         # Now prioritise it on mining_node, but short by one zatoshi.
-        priority_success = mining_node.prioritisetransaction(tx, 0, MARGINAL_FEE - fee_zats - 1)
+        priority_success = mining_node.prioritisetransaction(tx, 0, required_fee - fee_zats - 1)
         assert(priority_success)
         mempool = mining_node.getrawmempool(True)
-        assert_equal(MARGINAL_FEE - 1, mempool[tx]['modifiedfee'] * COIN)
+        assert_equal(required_fee - 1, mempool[tx]['modifiedfee'] * COIN)
         send_fully_paid_transaction()
         assert_equal(eventually_in_template(tx), False)
 
@@ -152,7 +178,7 @@ def sync_and_check():
         priority_success = mining_node.prioritisetransaction(tx, None, 1)
         assert(priority_success)
         mempool = mining_node.getrawmempool(True)
-        assert_equal(MARGINAL_FEE, mempool[tx]['modifiedfee'] * COIN)
+        assert_equal(required_fee, mempool[tx]['modifiedfee'] * COIN)
 
         # The block template will refresh after 1 minute, or after 5 seconds if a new
         # transaction is added to the mempool. As long as there is less than a minute
@@ -164,18 +190,17 @@ def sync_and_check():
         send_fully_paid_transaction()
         assert_equal(eventually_in_template(tx), True)
 
-        # Mine a block on node 0.
+        # Mine a block.
         blk_hash = mining_node.generate(1)
         block = mining_node.getblock(blk_hash[0])
         assert_equal(tx in block['tx'], True)
         sync_and_check()
 
-        # Check that tx was mined and that node 1 received the funds.
+        # Check that tx was mined and that node 3 received the expected funds (including
+        # the amounts from the three calls to `send_fully_paid_transaction`).
         mempool = mining_node.getrawmempool()
         assert_equal(mempool, [])
-        assert_equal(self.nodes[2].getbalance(), node2_initial_balance + amount)
-        # Check that all of the fully paid transactions were mined.
-        assert_equal(self.nodes[3].getbalance(), node3_initial_balance + Decimal("0.3"))
+        assert_equal(self.nodes[3].getbalance(), node3_initial_balance + amount + Decimal("0.3"))
 
 if __name__ == '__main__':
     PrioritiseTransactionTest().main()
diff --git a/qa/rpc-tests/regtest_signrawtransaction.py b/qa/rpc-tests/regtest_signrawtransaction.py
index df9512b330a..d79868114c2 100755
--- a/qa/rpc-tests/regtest_signrawtransaction.py
+++ b/qa/rpc-tests/regtest_signrawtransaction.py
@@ -1,14 +1,14 @@
 #!/usr/bin/env python3
-# Copyright (c) 2018 The Zcash developers
+# Copyright (c) 2018-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
 from test_framework.test_framework import BitcoinTestFramework
 from test_framework.util import (
-    LEGACY_DEFAULT_FEE,
     start_nodes,
     wait_and_assert_operationid_status,
 )
+from test_framework.zip317 import ZIP_317_FEE
 
 class RegtestSignrawtransactionTest (BitcoinTestFramework):
 
@@ -33,7 +33,7 @@ def run_test(self):
         opid = self.nodes[1].z_sendmany(
             taddr,
             [{'address': zaddr1, 'amount': 1}],
-            1, LEGACY_DEFAULT_FEE, 'AllowFullyTransparent')
+            1, ZIP_317_FEE, 'AllowFullyTransparent')
         wait_and_assert_operationid_status(self.nodes[1], opid)
 
 if __name__ == '__main__':
diff --git a/qa/rpc-tests/remove_sprout_shielding.py b/qa/rpc-tests/remove_sprout_shielding.py
index 99d82f7f0b7..fbe8bee14c3 100755
--- a/qa/rpc-tests/remove_sprout_shielding.py
+++ b/qa/rpc-tests/remove_sprout_shielding.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright (c) 2020 The Zcash developers
+# Copyright (c) 2020-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
@@ -17,11 +17,11 @@
     CANOPY_BRANCH_ID,
     NU5_BRANCH_ID,
 )
+from test_framework.zip317 import ZIP_317_FEE
 
 import logging
 
 HAS_CANOPY = [
-    '-minrelaytxfee=0',
     '-nurejectoldversions=false',
     '-anchorconfirmations=1',
     nuparams(BLOSSOM_BRANCH_ID, 205),
@@ -69,7 +69,7 @@ def run_test(self):
         n0_taddr0 = self.nodes[0].getnewaddress()
         for _ in range(3):
             recipients = [{"address": n0_taddr0, "amount": Decimal('1')}]
-            myopid = self.nodes[0].z_sendmany(n0_sprout_addr0, recipients, 1, 0, 'AllowRevealedRecipients')
+            myopid = self.nodes[0].z_sendmany(n0_sprout_addr0, recipients, 1, ZIP_317_FEE, 'AllowRevealedRecipients')
             wait_and_assert_operationid_status(self.nodes[0], myopid)
             self.sync_all()
             self.nodes[0].generate(1)
@@ -82,14 +82,14 @@ def run_test(self):
             JSONRPCException,
             "Sending funds into the Sprout pool is no longer supported.",
             self.nodes[0].z_mergetoaddress,
-            ["ANY_TADDR"], n1_sprout_addr0, 0)
+            ["ANY_TADDR"], n1_sprout_addr0, ZIP_317_FEE)
 
         self.nodes[0].generate(1)
         self.sync_all()
 
         # Send some funds back to n0_taddr0
         recipients = [{"address": n0_taddr0, "amount": Decimal('1')}]
-        myopid = self.nodes[0].z_sendmany(n0_sprout_addr0, recipients, 1, 0, 'AllowRevealedRecipients')
+        myopid = self.nodes[0].z_sendmany(n0_sprout_addr0, recipients, 1, ZIP_317_FEE, 'AllowRevealedRecipients')
         wait_and_assert_operationid_status(self.nodes[0], myopid)
 
         # Mine to one block before Canopy activation on node 0; adding value
@@ -114,7 +114,7 @@ def run_test(self):
         # Create taddr -> Sprout z_sendmany transaction on node 0. Should fail
         n1_sprout_addr1 = self.nodes[1].z_getnewaddress('sprout')
         recipients = [{"address": n1_sprout_addr1, "amount": Decimal('1')}]
-        myopid = self.nodes[0].z_sendmany(n0_taddr0, recipients, 1, 0)
+        myopid = self.nodes[0].z_sendmany(n0_taddr0, recipients, 1, ZIP_317_FEE)
         wait_and_assert_operationid_status(self.nodes[0], myopid, "failed", unsupported_sprout_msg)
         print("taddr -> Sprout z_sendmany tx rejected at Canopy activation on node 0")
 
@@ -139,7 +139,7 @@ def run_test(self):
 
         # Shield coinbase to Sapling on node 0. Should pass
         sapling_addr = self.nodes[0].z_getnewaddress('sapling')
-        myopid = self.nodes[0].z_shieldcoinbase(get_coinbase_address(self.nodes[0]), sapling_addr, 0)['opid']
+        myopid = self.nodes[0].z_shieldcoinbase(get_coinbase_address(self.nodes[0]), sapling_addr, ZIP_317_FEE)['opid']
         wait_and_assert_operationid_status(self.nodes[0], myopid)
         print("taddr -> Sapling z_shieldcoinbase tx accepted after Canopy on node 0")
 
diff --git a/qa/rpc-tests/shorter_block_times.py b/qa/rpc-tests/shorter_block_times.py
index f96dc3a53d9..db724a1a7de 100755
--- a/qa/rpc-tests/shorter_block_times.py
+++ b/qa/rpc-tests/shorter_block_times.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright (c) 2019 The Zcash developers
+# Copyright (c) 2019-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
@@ -13,6 +13,7 @@
     start_nodes,
     wait_and_assert_operationid_status,
 )
+from test_framework.zip317 import conventional_fee
 
 
 class ShorterBlockTimes(BitcoinTestFramework):
@@ -23,7 +24,6 @@ def __init__(self):
 
     def setup_nodes(self):
         return start_nodes(self.num_nodes, self.options.tmpdir, extra_args=[[
-            '-minrelaytxfee=0',
             nuparams(BLOSSOM_BRANCH_ID, 106),
             '-allowdeprecated=z_getnewaddress',
             '-allowdeprecated=z_gettotalbalance',
@@ -39,14 +39,15 @@ def run_test(self):
 
         node0_taddr = get_coinbase_address(self.nodes[0])
         node0_zaddr = self.nodes[0].z_getnewaddress('sapling')
-        recipients = [{'address': node0_zaddr, 'amount': Decimal('10.0')}]
-        myopid = self.nodes[0].z_sendmany(node0_taddr, recipients, 1, 0, 'AllowRevealedSenders')
+        fee = conventional_fee(3)
+        recipients = [{'address': node0_zaddr, 'amount': Decimal('10.0') - fee}]
+        myopid = self.nodes[0].z_sendmany(node0_taddr, recipients, 1, fee, 'AllowRevealedSenders')
         txid = wait_and_assert_operationid_status(self.nodes[0], myopid)
         assert_equal(105, self.nodes[0].getrawtransaction(txid, 1)['expiryheight'])  # Blossom activation - 1
         self.sync_all()
         self.nodes[0].generate(1)
         self.sync_all()
-        assert_equal(10, Decimal(self.nodes[0].z_gettotalbalance()['private']))
+        assert_equal(10 - fee, Decimal(self.nodes[0].z_gettotalbalance()['private']))
 
         self.nodes[0].generate(2)
         self.sync_all()
@@ -65,13 +66,13 @@ def run_test(self):
         assert_equal(15, self.nodes[1].getwalletinfo()['immature_balance'])
 
         # Send and mine a transaction after activation
-        myopid = self.nodes[0].z_sendmany(node0_taddr, recipients, 1, 0, 'AllowRevealedSenders')
+        myopid = self.nodes[0].z_sendmany(node0_taddr, recipients, 1, fee, 'AllowFullyTransparent')
         txid = wait_and_assert_operationid_status(self.nodes[0], myopid)
         assert_equal(147, self.nodes[0].getrawtransaction(txid, 1)['expiryheight'])  # height + 1 + 40
         self.sync_all() # Ensure the transaction has propagated to node 1
         self.nodes[1].generate(1)
         self.sync_all()
-        assert_equal(20, Decimal(self.nodes[0].z_gettotalbalance()['private']))
+        assert_equal(20 - fee*2, Decimal(self.nodes[0].z_gettotalbalance()['private']))
 
 
 if __name__ == '__main__':
diff --git a/qa/rpc-tests/show_help.py b/qa/rpc-tests/show_help.py
index 69597cf95a8..b1eab378af8 100755
--- a/qa/rpc-tests/show_help.py
+++ b/qa/rpc-tests/show_help.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 # Copyright (c) 2014-2016 The Bitcoin Core developers
-# Copyright (c) 2017-2022 The Zcash developers
+# Copyright (c) 2017-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
@@ -502,7 +502,7 @@
 
   -txunpaidactionlimit=<n>
        Transactions with more than this number of unpaid actions will not be
-       accepted to the mempool or relayed (default: 50)
+       accepted to the mempool or relayed (default: 0)
 
 Block creation options:
 
@@ -511,7 +511,7 @@
 
   -blockunpaidactionlimit=<n>
        Set the limit on unpaid actions that will be accepted in a block for
-       transactions paying less than the ZIP 317 fee (default: 50)
+       transactions paying less than the ZIP 317 fee (default: 0)
 
 |  -blockversion=<n>
 |       Override block version to test forking scenarios (default: 4)
diff --git a/qa/rpc-tests/signrawtransaction_offline.py b/qa/rpc-tests/signrawtransaction_offline.py
index 8927c6eb7d5..5f58f70c7a5 100755
--- a/qa/rpc-tests/signrawtransaction_offline.py
+++ b/qa/rpc-tests/signrawtransaction_offline.py
@@ -1,15 +1,18 @@
 #!/usr/bin/env python3
+# Copyright (c) 2018-2024 The Zcash developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
 from test_framework.test_framework import BitcoinTestFramework
 from test_framework.util import (
     BLOSSOM_BRANCH_ID,
-    LEGACY_DEFAULT_FEE,
     assert_equal,
     assert_true,
     initialize_chain_clean,
     nuparams,
     start_node,
 )
+from test_framework.zip317 import conventional_fee
 from test_framework.authproxy import JSONRPCException
 
 from decimal import Decimal
@@ -49,7 +52,7 @@ def run_test(self):
         create_inputs = [{'txid': txid, 'vout': 0}]
         sign_inputs = [{'txid': txid, 'vout': 0, 'scriptPubKey': scriptpubkey, 'amount': 10}]
 
-        create_hex = self.nodes[0].createrawtransaction(create_inputs, {taddr: Decimal('10.0') - LEGACY_DEFAULT_FEE})
+        create_hex = self.nodes[0].createrawtransaction(create_inputs, {taddr: Decimal('10.0') - conventional_fee(2)})
 
         # An offline regtest node does not rely on the approx release height of the software
         # to determine the consensus rules to be used for signing.
diff --git a/qa/rpc-tests/sprout_sapling_migration.py b/qa/rpc-tests/sprout_sapling_migration.py
index aee4370cb76..1fec8a667f7 100755
--- a/qa/rpc-tests/sprout_sapling_migration.py
+++ b/qa/rpc-tests/sprout_sapling_migration.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright (c) 2019 The Zcash developers
+# Copyright (c) 2019-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
@@ -7,7 +7,8 @@
 from test_framework.test_framework import BitcoinTestFramework
 from test_framework.util import assert_equal, assert_true, \
     start_nodes, \
-    wait_and_assert_operationid_status_result, LEGACY_DEFAULT_FEE
+    wait_and_assert_operationid_status_result
+from test_framework.zip317 import conventional_fee
 
 SAPLING_ADDR = 'zregtestsapling1ssqj3f3majnl270985gqcdqedd9t4nlttjqskccwevj2v20sc25deqspv3masufnwcdy67cydyy'
 SAPLING_KEY = 'secret-extended-key-regtest1qv62zt2fqyqqpqrh2qzc08h7gncf4447jh9kvnnnhjg959fkwt7mhw9j8e9at7attx8z6u3953u86vcnsujdc2ckdlcmztjt44x3uxpah5mxtncxd0mqcnz9eq8rghh5m4j44ep5d9702sdvvwawqassulktfegrcp4twxgqdxx4eww3lau0mywuaeztpla2cmvagr5nj98elt45zh6fjznadl6wz52n2uyhdwcm2wlsu8fnxstrk6s4t55t8dy6jkgx5g0cwpchh5qffp8x5'
@@ -127,7 +128,8 @@ def run_migration_test(self, node, sproutAddr, saplingAddr, target_height, sprou
         # Check that unmigrated amount + unfinalized = starting balance - fee
         status = node.z_getmigrationstatus()
         print("status: {}".format(status))
-        assert_equal(sprout_initial_balance - LEGACY_DEFAULT_FEE, Decimal(status['unmigrated_amount']) + Decimal(status['unfinalized_migrated_amount']))
+        fee = conventional_fee(4)
+        assert_equal(sprout_initial_balance - fee, Decimal(status['unmigrated_amount']) + Decimal(status['unfinalized_migrated_amount']))
 
         # The transaction in the mempool should be the one listed in migration_txids,
         # and it should expire at the next 450 % 500.
@@ -146,7 +148,7 @@ def run_migration_test(self, node, sproutAddr, saplingAddr, target_height, sprou
         print("sprout balance: {}, sapling balance: {}".format(sprout_balance, sapling_balance))
         assert_true(sprout_balance < sprout_initial_balance, "Should have less Sprout funds")
         assert_true(sapling_balance > Decimal('0'), "Should have more Sapling funds")
-        assert_true(sprout_balance + sapling_balance, sprout_initial_balance - LEGACY_DEFAULT_FEE)
+        assert_true(sprout_balance + sapling_balance, sprout_initial_balance - fee)
 
         check_migration_status(node, saplingAddr, DURING_MIGRATION)
         # At 10 % 500 the transactions will be considered 'finalized'
diff --git a/qa/rpc-tests/test_framework/blocktools.py b/qa/rpc-tests/test_framework/blocktools.py
index 9c6fa430d2d..61572825364 100644
--- a/qa/rpc-tests/test_framework/blocktools.py
+++ b/qa/rpc-tests/test_framework/blocktools.py
@@ -1,7 +1,7 @@
 #!/usr/bin/env python3
 # blocktools.py - utilities for manipulating blocks and transactions
 # Copyright (c) 2015-2016 The Bitcoin Core developers
-# Copyright (c) 2017-2022 The Zcash developers
+# Copyright (c) 2017-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
@@ -101,10 +101,10 @@ def create_coinbase(height, pubkey=None, after_blossom=False, outputs=[], lockbo
 
 # Create a transaction with an anyone-can-spend output, that spends the
 # nth output of prevtx.
-def create_transaction(prevtx, n, sig, value):
+def create_transaction(prevtx, n, sig, value_zats):
     tx = CTransaction()
     assert(n < len(prevtx.vout))
     tx.vin.append(CTxIn(COutPoint(prevtx.sha256, n), sig, 0xffffffff))
-    tx.vout.append(CTxOut(value, b""))
+    tx.vout.append(CTxOut(value_zats, b""))
     tx.calc_sha256()
     return tx
diff --git a/qa/rpc-tests/test_framework/util.py b/qa/rpc-tests/test_framework/util.py
index fd39336592f..2481baa3f65 100644
--- a/qa/rpc-tests/test_framework/util.py
+++ b/qa/rpc-tests/test_framework/util.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 # Copyright (c) 2014-2016 The Bitcoin Core developers
-# Copyright (c) 2016-2022 The Zcash developers
+# Copyright (c) 2016-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
@@ -31,8 +31,6 @@
 
 ZCASHD_BINARY = os.path.join('src', 'zcashd')
 
-LEGACY_DEFAULT_FEE = Decimal('0.00001')
-
 COVERAGE_DIR = None
 PRE_BLOSSOM_BLOCK_TARGET_SPACING = 150
 POST_BLOSSOM_BLOCK_TARGET_SPACING = 75
diff --git a/qa/rpc-tests/test_framework/zip317.py b/qa/rpc-tests/test_framework/zip317.py
index 7e8cd904a7b..9adb2ad3357 100644
--- a/qa/rpc-tests/test_framework/zip317.py
+++ b/qa/rpc-tests/test_framework/zip317.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright (c) 2023 The Zcash developers
+# Copyright (c) 2023-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
@@ -19,6 +19,9 @@
 # https://zips.z.cash/zip-0317#fee-calculation.
 GRACE_ACTIONS = 2
 
+# The minimum ZIP 317 fee.
+MINIMUM_FEE = MARGINAL_FEE * GRACE_ACTIONS
+
 # Limits the relative probability of picking a given transaction to be at most `WEIGHT_RATIO_CAP`
 # times greater than a transaction that pays exactly the conventional fee. See
 # https://zips.z.cash/zip-0317#recommended-algorithm-for-block-template-construction
@@ -26,7 +29,7 @@
 
 # Default limit on the number of unpaid actions in a block. See
 # https://zips.z.cash/zip-0317#recommended-algorithm-for-block-template-construction
-DEFAULT_BLOCK_UNPAID_ACTION_LIMIT = 50
+DEFAULT_BLOCK_UNPAID_ACTION_LIMIT = 0
 
 # The zcashd RPC sentinel value to indicate the conventional_fee when a positional argument is
 # required.
diff --git a/qa/rpc-tests/turnstile.py b/qa/rpc-tests/turnstile.py
index e904c4dac63..9b9343ee6ea 100755
--- a/qa/rpc-tests/turnstile.py
+++ b/qa/rpc-tests/turnstile.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright (c) 2019 The Zcash developers
+# Copyright (c) 2019-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
@@ -38,10 +38,11 @@
     bitcoind_processes,
     check_node_log
 )
+from test_framework.zip317 import conventional_fee, ZIP_317_FEE
+
 from decimal import Decimal
 
 BASE_ARGS = [
-    '-minrelaytxfee=0',
     '-allowdeprecated=z_getnewaddress',
     '-allowdeprecated=z_getbalance',
 ]
@@ -91,14 +92,14 @@ def run_test(self):
         self.sync_all()
 
         taddr0 = get_coinbase_address(self.nodes[0])
+        fee = conventional_fee(3)
         if (POOL_NAME == "SPROUT"):
             dest_addr = self.nodes[0].listaddresses()[0]['sprout']['addresses'][0]
         elif (POOL_NAME == "SAPLING"):
             # Node 0 shields some funds
             dest_addr = self.nodes[0].z_getnewaddress('sapling')
-            recipients = []
-            recipients.append({"address": dest_addr, "amount": Decimal('50')})
-            myopid = self.nodes[0].z_sendmany(taddr0, recipients, 1, 0)
+            recipients = [{"address": dest_addr, "amount": Decimal('50') - fee}]
+            myopid = self.nodes[0].z_sendmany(taddr0, recipients, 1, fee)
             wait_and_assert_operationid_status(self.nodes[0], myopid)
         else:
             fail("Unrecognized pool name: " + POOL_NAME)
@@ -121,9 +122,8 @@ def run_test(self):
         self.assert_pool_balance(self.nodes[2], POOL_NAME.lower(), Decimal('200'))
 
         # Node 0 creates an unshielding transaction
-        recipients = []
-        recipients.append({"address": taddr0, "amount": Decimal('1')})
-        myopid = self.nodes[0].z_sendmany(dest_addr, recipients, 1, 0, 'AllowRevealedRecipients')
+        recipients = [{"address": taddr0, "amount": Decimal('1')}]
+        myopid = self.nodes[0].z_sendmany(dest_addr, recipients, 1, ZIP_317_FEE, 'AllowRevealedRecipients')
         mytxid = wait_and_assert_operationid_status(self.nodes[0], myopid)
 
         # Verify transaction appears in mempool of nodes
@@ -170,8 +170,8 @@ def run_test(self):
 
         # Verify size of shielded pool
         self.assert_pool_balance(self.nodes[0], POOL_NAME.lower(), Decimal('0'))
-        self.assert_pool_balance(self.nodes[1], POOL_NAME.lower(), Decimal('199'))
-        self.assert_pool_balance(self.nodes[2], POOL_NAME.lower(), Decimal('199'))
+        self.assert_pool_balance(self.nodes[1], POOL_NAME.lower(), Decimal('199') - fee)
+        self.assert_pool_balance(self.nodes[2], POOL_NAME.lower(), Decimal('199') - fee)
 
         # Stop node 0 and check logs to verify the block was rejected as a turnstile violation
         string_to_find1 = "ConnectBlock: turnstile violation in " + POOL_NAME.capitalize() + " shielded value pool"
diff --git a/qa/rpc-tests/txn_doublespend.py b/qa/rpc-tests/txn_doublespend.py
index 3e55edb4a90..3d06f79b045 100755
--- a/qa/rpc-tests/txn_doublespend.py
+++ b/qa/rpc-tests/txn_doublespend.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 # Copyright (c) 2014-2016 The Bitcoin Core developers
-# Copyright (c) 2016-2022 The Zcash developers
+# Copyright (c) 2016-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
@@ -16,6 +16,7 @@
     sync_blocks,
     gather_inputs,
 )
+from test_framework.zip317 import conventional_fee
 
 
 class TxnMallTest(BitcoinTestFramework):
@@ -26,7 +27,6 @@ def add_options(self, parser):
 
     def setup_nodes(self):
         return start_nodes(self.num_nodes, self.options.tmpdir, extra_args=[[
-            '-minrelaytxfee=0',
             '-allowdeprecated=getnewaddress',
         ]] * self.num_nodes)
 
@@ -48,10 +48,12 @@ def run_test(self):
         # First: use raw transaction API to send (starting_balance - (mining_reward - 2)) ZEC to node1_address,
         # but don't broadcast:
         (total_in, inputs) = gather_inputs(self.nodes[0], (starting_balance - (mining_reward - 2)))
+        fee = conventional_fee(25)
         change_address = self.nodes[0].getnewaddress("")
-        outputs = {}
-        outputs[change_address] = (mining_reward - 2)
-        outputs[node1_address] = (starting_balance - (mining_reward - 2))
+        outputs = {
+            change_address: mining_reward - 2 - fee,
+            node1_address: starting_balance - (mining_reward - 2),
+        }
         rawtx = self.nodes[0].createrawtransaction(inputs, outputs)
         doublespend = self.nodes[0].signrawtransaction(rawtx)
         assert_equal(doublespend["complete"], True)
@@ -82,7 +84,7 @@ def run_test(self):
             assert_equal(tx1["confirmations"], 1)
             assert_equal(tx2["confirmations"], 1)
             # Node1's total balance should be its starting balance plus both transaction amounts:
-            assert_equal(self.nodes[1].getbalance("*"), starting_balance - (tx1["amount"]+tx2["amount"]))
+            assert_equal(self.nodes[1].getbalance("*"), starting_balance - (tx1["amount"] + tx2["amount"]))
         else:
             assert_equal(tx1["confirmations"], 0)
             assert_equal(tx2["confirmations"], 0)
@@ -105,9 +107,9 @@ def run_test(self):
         assert_equal(tx1["confirmations"], -1)
         assert_equal(tx2["confirmations"], -1)
 
-        # Node0's total balance should be starting balance, plus (mining_reward * 2) for
-        # two more matured blocks, minus (starting_balance - (mining_reward - 2)) for the double-spend:
-        expected = starting_balance + (mining_reward * 2) - (starting_balance - (mining_reward - 2))
+        # Node0's total balance should be starting balance, plus (mining_reward * 2) for two more
+        # matured blocks, minus (starting_balance - (mining_reward - 2) + fee) for the double-spend.
+        expected = starting_balance + (mining_reward * 2) - (starting_balance - (mining_reward - 2) + fee)
         assert_equal(self.nodes[0].getbalance(), expected)
         assert_equal(self.nodes[0].getbalance("*"), expected)
 
diff --git a/qa/rpc-tests/wallet.py b/qa/rpc-tests/wallet.py
index bac5d5b2b08..bda68b2d337 100755
--- a/qa/rpc-tests/wallet.py
+++ b/qa/rpc-tests/wallet.py
@@ -1,11 +1,12 @@
 #!/usr/bin/env python3
 # Copyright (c) 2014-2016 The Bitcoin Core developers
-# Copyright (c) 2016-2022 The Zcash developers
+# Copyright (c) 2016-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
 from test_framework.test_framework import BitcoinTestFramework
 from test_framework.authproxy import JSONRPCException
+from test_framework.mininode import COIN
 from test_framework.util import assert_equal, start_nodes, start_node, \
     connect_nodes_bi, sync_blocks, sync_mempools
 from test_framework.zip317 import conventional_fee
@@ -21,7 +22,6 @@ def __init__(self):
 
     def setup_network(self, split=False):
         self.nodes = start_nodes(3, self.options.tmpdir, extra_args=[[
-            '-minrelaytxfee=0',
             '-allowdeprecated=getnewaddress',
             '-allowdeprecated=z_getbalance',
         ]] * 3)
@@ -31,15 +31,15 @@ def setup_network(self, split=False):
         self.is_network_split=False
         self.sync_all()
 
-    def run_test (self):
+    def run_test(self):
         print("Mining blocks...")
 
         self.nodes[0].generate(4)
         self.sync_all()
 
         walletinfo = self.nodes[0].getwalletinfo()
-        assert_equal(walletinfo['immature_balance'], 40)
-        assert_equal(walletinfo['balance'], 0)
+        assert_equal(Decimal(walletinfo['immature_balance']), Decimal('40'))
+        assert_equal(Decimal(walletinfo['balance']), Decimal('0'))
 
         blockchaininfo = self.nodes[0].getblockchaininfo()
         assert_equal(blockchaininfo['estimatedheight'], 4)
@@ -48,20 +48,20 @@ def run_test (self):
         self.nodes[1].generate(101)
         self.sync_all()
 
-        assert_equal(self.nodes[0].getbalance(), 40)
-        assert_equal(self.nodes[1].getbalance(), 10)
-        assert_equal(self.nodes[2].getbalance(), 0)
-        assert_equal(self.nodes[0].getbalance("*"), 40)
-        assert_equal(self.nodes[1].getbalance("*"), 10)
-        assert_equal(self.nodes[2].getbalance("*"), 0)
+        assert_equal(Decimal(self.nodes[0].getbalance()), Decimal('40'))
+        assert_equal(Decimal(self.nodes[1].getbalance()), Decimal('10'))
+        assert_equal(Decimal(self.nodes[2].getbalance()), Decimal('0'))
+        assert_equal(Decimal(self.nodes[0].getbalance("*")), Decimal('40'))
+        assert_equal(Decimal(self.nodes[1].getbalance("*")), Decimal('10'))
+        assert_equal(Decimal(self.nodes[2].getbalance("*")), Decimal('0'))
 
         # Send 21 ZEC from 0 to 2 using sendtoaddress call.
         # Second transaction will be child of first, and will require a fee
-        self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 11)
-        self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 10)
+        self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), Decimal('11'))
+        self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), Decimal('10'))
 
         walletinfo = self.nodes[0].getwalletinfo()
-        assert_equal(walletinfo['immature_balance'], 0)
+        assert_equal(Decimal(walletinfo['immature_balance']), Decimal('0'))
 
         blockchaininfo = self.nodes[0].getblockchaininfo()
         assert_equal(blockchaininfo['estimatedheight'], 105)
@@ -77,10 +77,10 @@ def run_test (self):
 
         # node0 should end up with 50 ZEC in block rewards plus fees, but
         # minus the 21 ZEC plus fees sent to node2
-        assert_equal(self.nodes[0].getbalance(), 50-21)
-        assert_equal(self.nodes[2].getbalance(), 21)
-        assert_equal(self.nodes[0].getbalance("*"), 50-21)
-        assert_equal(self.nodes[2].getbalance("*"), 21)
+        assert_equal(Decimal(self.nodes[0].getbalance()), Decimal('50') - Decimal('21'))
+        assert_equal(Decimal(self.nodes[2].getbalance()), Decimal('21'))
+        assert_equal(Decimal(self.nodes[0].getbalance("*")), Decimal('50') - Decimal('21'))
+        assert_equal(Decimal(self.nodes[2].getbalance("*")), Decimal('21'))
 
         # Node0 should have three unspent outputs.
         # Create a couple of transactions to send them to node2, submit them through
@@ -102,16 +102,14 @@ def run_test (self):
 
         # Catch an attempt to send a transaction with an absurdly high fee.
         # Send 1.0 ZEC from an utxo of value 10.0 ZEC but don't specify a change output, so then
-        # the change of 9.0 ZEC becomes the fee, which is considered to be absurdly high because
-        # the fee is more than 4 times the conventional fee.
-        assert(Decimal("9.0") > 4*conventional_fee(1))
+        # the change of 9.0 ZEC becomes the fee, which is considered to be absurdly high.
         inputs = []
         outputs = {}
         for utxo in node2utxos:
             if utxo["amount"] == Decimal("10.0"):
                 break
         assert_equal(utxo["amount"], Decimal("10.0"))
-        inputs.append({ "txid" : utxo["txid"], "vout" : utxo["vout"]})
+        inputs.append({"txid": utxo["txid"], "vout": utxo["vout"]})
         outputs[self.nodes[2].getnewaddress("")] = Decimal("1.0")
         raw_tx = self.nodes[2].createrawtransaction(inputs, outputs)
         signed_tx = self.nodes[2].signrawtransaction(raw_tx)
@@ -123,11 +121,12 @@ def run_test (self):
 
         # create both transactions
         txns_to_send = []
+        fee = conventional_fee(2)
         for utxo in node0utxos:
             inputs = []
             outputs = {}
-            inputs.append({ "txid" : utxo["txid"], "vout" : utxo["vout"]})
-            outputs[self.nodes[2].getnewaddress("")] = utxo["amount"]
+            inputs.append({"txid": utxo["txid"], "vout": utxo["vout"]})
+            outputs[self.nodes[2].getnewaddress("")] = utxo["amount"] - fee
             raw_tx = self.nodes[0].createrawtransaction(inputs, outputs)
             txns_to_send.append(self.nodes[0].signrawtransaction(raw_tx))
 
@@ -141,52 +140,52 @@ def run_test (self):
         self.nodes[1].generate(1)
         self.sync_all()
 
-        assert_equal(self.nodes[0].getbalance(), 0)
-        assert_equal(self.nodes[2].getbalance(), 50)
-        assert_equal(self.nodes[0].getbalance("*"), 0)
-        assert_equal(self.nodes[2].getbalance("*"), 50)
+        assert_equal(Decimal(self.nodes[0].getbalance()), Decimal('0'))
+        assert_equal(Decimal(self.nodes[2].getbalance()), Decimal('50') - 3*fee)
+        assert_equal(Decimal(self.nodes[0].getbalance("*")), Decimal('0'))
+        assert_equal(Decimal(self.nodes[2].getbalance("*")), Decimal('50') - 3*fee)
 
         # Send 10 ZEC normally
         address = self.nodes[0].getnewaddress("")
         self.nodes[2].settxfee(Decimal('0.001'))  # not the default
-        self.nodes[2].sendtoaddress(address, 10, "", "", False)
+        self.nodes[2].sendtoaddress(address, Decimal('10'), "", "", False)
         self.sync_all()
         self.nodes[2].generate(1)
         self.sync_all()
-        assert_equal(self.nodes[2].getbalance(), Decimal('39.99900000'))
-        assert_equal(self.nodes[0].getbalance(), Decimal('10.00000000'))
-        assert_equal(self.nodes[2].getbalance("*"), Decimal('39.99900000'))
-        assert_equal(self.nodes[0].getbalance("*"), Decimal('10.00000000'))
+        assert_equal(Decimal(self.nodes[2].getbalance()), Decimal('39.99900000') - 3*fee)
+        assert_equal(Decimal(self.nodes[0].getbalance()), Decimal('10.00000000'))
+        assert_equal(Decimal(self.nodes[2].getbalance("*")), Decimal('39.99900000') - 3*fee)
+        assert_equal(Decimal(self.nodes[0].getbalance("*")), Decimal('10.00000000'))
 
         # Send 10 ZEC with subtract fee from amount
-        self.nodes[2].sendtoaddress(address, 10, "", "", True)
+        self.nodes[2].sendtoaddress(address, Decimal('10'), "", "", True)
         self.sync_all()
         self.nodes[2].generate(1)
         self.sync_all()
-        assert_equal(self.nodes[2].getbalance(), Decimal('29.99900000'))
-        assert_equal(self.nodes[0].getbalance(), Decimal('19.99900000'))
-        assert_equal(self.nodes[2].getbalance("*"), Decimal('29.99900000'))
-        assert_equal(self.nodes[0].getbalance("*"), Decimal('19.99900000'))
+        assert_equal(Decimal(self.nodes[2].getbalance()), Decimal('29.99900000') - 3*fee)
+        assert_equal(Decimal(self.nodes[0].getbalance()), Decimal('19.99900000'))
+        assert_equal(Decimal(self.nodes[2].getbalance("*")), Decimal('29.99900000') - 3*fee)
+        assert_equal(Decimal(self.nodes[0].getbalance("*")), Decimal('19.99900000'))
 
         # Sendmany 10 ZEC
-        self.nodes[2].sendmany("", {address: 10}, 0, "", [])
+        self.nodes[2].sendmany("", {address: Decimal('10')}, 0, "", [])
         self.sync_all()
         self.nodes[2].generate(1)
         self.sync_all()
-        assert_equal(self.nodes[2].getbalance(), Decimal('19.99800000'))
-        assert_equal(self.nodes[0].getbalance(), Decimal('29.99900000'))
-        assert_equal(self.nodes[2].getbalance("*"), Decimal('19.99800000'))
-        assert_equal(self.nodes[0].getbalance("*"), Decimal('29.99900000'))
+        assert_equal(Decimal(self.nodes[2].getbalance()), Decimal('19.99800000') - 3*fee)
+        assert_equal(Decimal(self.nodes[0].getbalance()), Decimal('29.99900000'))
+        assert_equal(Decimal(self.nodes[2].getbalance("*")), Decimal('19.99800000') - 3*fee)
+        assert_equal(Decimal(self.nodes[0].getbalance("*")), Decimal('29.99900000'))
 
         # Sendmany 10 ZEC with subtract fee from amount
-        self.nodes[2].sendmany("", {address: 10}, 0, "", [address])
+        self.nodes[2].sendmany("", {address: Decimal('10')}, 0, "", [address])
         self.sync_all()
         self.nodes[2].generate(1)
         self.sync_all()
-        assert_equal(self.nodes[2].getbalance(), Decimal('9.99800000'))
-        assert_equal(self.nodes[0].getbalance(), Decimal('39.99800000'))
-        assert_equal(self.nodes[2].getbalance("*"), Decimal('9.99800000'))
-        assert_equal(self.nodes[0].getbalance("*"), Decimal('39.99800000'))
+        assert_equal(Decimal(self.nodes[2].getbalance()), Decimal('9.99800000') - 3*fee)
+        assert_equal(Decimal(self.nodes[0].getbalance()), Decimal('39.99800000'))
+        assert_equal(Decimal(self.nodes[2].getbalance("*")), Decimal('9.99800000') - 3*fee)
+        assert_equal(Decimal(self.nodes[0].getbalance("*")), Decimal('39.99800000'))
 
         # Test ResendWalletTransactions:
         # Create a couple of transactions, then start up a fourth
@@ -207,16 +206,16 @@ def run_test (self):
         assert(txid1 in self.nodes[3].getrawmempool())
 
         # check integer balances from getbalance
-        assert_equal(self.nodes[2].getbalance("*", 1, False, True), 999800000)
+        assert_equal(Decimal(self.nodes[2].getbalance("*", 1, False, True)), 999800000 - 3*fee*COIN)
 
         # send from node 0 to node 2 taddr
         mytaddr = self.nodes[2].getnewaddress()
-        mytxid = self.nodes[0].sendtoaddress(mytaddr, 10.0)
+        mytxid = self.nodes[0].sendtoaddress(mytaddr, Decimal('10.0'))
         self.sync_all()
         self.nodes[0].generate(1)
         self.sync_all()
 
-        mybalance = self.nodes[2].z_getbalance(mytaddr)
+        mybalance = Decimal(self.nodes[2].z_getbalance(mytaddr))
         assert_equal(mybalance, Decimal('10.0'))
 
         # check integer balances from z_getbalance
diff --git a/qa/rpc-tests/wallet_1941.py b/qa/rpc-tests/wallet_1941.py
index d9bcb405f16..0005c435cd9 100755
--- a/qa/rpc-tests/wallet_1941.py
+++ b/qa/rpc-tests/wallet_1941.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright (c) 2016 The Zcash developers
+# Copyright (c) 2016-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
@@ -9,7 +9,8 @@
 from test_framework.util import assert_equal, initialize_chain_clean, \
     initialize_datadir, start_nodes, start_node, connect_nodes_bi, \
     bitcoind_processes, wait_and_assert_operationid_status, \
-    get_coinbase_address, LEGACY_DEFAULT_FEE
+    get_coinbase_address
+from test_framework.zip317 import conventional_fee
 
 from decimal import Decimal
 
@@ -67,9 +68,9 @@ def run_test (self):
         myzaddr = self.nodes[0].z_getnewaddress()
 
         # Send 10 coins to our zaddr.
-        recipients = []
-        recipients.append({"address":myzaddr, "amount":Decimal('10.0') - LEGACY_DEFAULT_FEE})
-        myopid = self.nodes[0].z_sendmany(mytaddr, recipients, 1, LEGACY_DEFAULT_FEE, 'AllowRevealedSenders')
+        fee = conventional_fee(3)
+        recipients = [{"address": myzaddr, "amount": Decimal('10.0') - fee}]
+        myopid = self.nodes[0].z_sendmany(mytaddr, recipients, 1, fee, 'AllowRevealedSenders')
         wait_and_assert_operationid_status(self.nodes[0], myopid)
         self.nodes[0].generate(1)
 
@@ -82,7 +83,7 @@ def run_test (self):
 
         # Confirm the balance on node 0.
         resp = self.nodes[0].z_getbalance(myzaddr)
-        assert_equal(Decimal(resp), Decimal('10.0') - LEGACY_DEFAULT_FEE)
+        assert_equal(Decimal(resp), Decimal('10.0') - fee)
 
         # Export the key for the zaddr from node 0.
         key = self.nodes[0].z_exportkey(myzaddr)
@@ -109,7 +110,7 @@ def run_test (self):
         # Confirm that the balance on node 1 is valid now (node 1 must
         # have rescanned)
         resp = self.nodes[1].z_getbalance(myzaddr)
-        assert_equal(Decimal(resp), Decimal('10.0') - LEGACY_DEFAULT_FEE)
+        assert_equal(Decimal(resp), Decimal('10.0') - fee)
 
 
 if __name__ == '__main__':
diff --git a/qa/rpc-tests/wallet_accounts.py b/qa/rpc-tests/wallet_accounts.py
index bf5f657e4b3..e9d8b2f03ec 100755
--- a/qa/rpc-tests/wallet_accounts.py
+++ b/qa/rpc-tests/wallet_accounts.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright (c) 2022 The Zcash developers
+# Copyright (c) 2022-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
@@ -16,6 +16,7 @@
     start_nodes,
     wait_and_assert_operationid_status,
 )
+from test_framework.zip317 import conventional_fee
 
 from decimal import Decimal
 
@@ -23,7 +24,6 @@
 class WalletAccountsTest(BitcoinTestFramework):
     def setup_nodes(self):
         return start_nodes(self.num_nodes, self.options.tmpdir, extra_args=[[
-            '-minrelaytxfee=0',
             nuparams(NU5_BRANCH_ID, 210),
             '-allowdeprecated=z_getnewaddress',
             '-allowdeprecated=z_getbalance',
@@ -141,9 +141,11 @@ def run_test(self):
 
         # Send coinbase funds to the UA.
         print('Sending coinbase funds to account')
-        recipients = [{'address': ua0, 'amount': Decimal('10')}]
-        opid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, 0, 'AllowRevealedSenders')
+        coinbase_fee = conventional_fee(3)
+        recipients = [{'address': ua0, 'amount': Decimal('10') - coinbase_fee}]
+        opid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, coinbase_fee, 'AllowRevealedSenders')
         txid = wait_and_assert_operationid_status(self.nodes[0], opid)
+        ua0sapling_balance = Decimal('10') - coinbase_fee
 
         # The wallet should detect the new note as belonging to the UA.
         tx_details = self.nodes[0].z_viewtransaction(txid)
@@ -155,21 +157,23 @@ def run_test(self):
         # visible with minconf=0.
         self.sync_all()
         self.check_balance(0, 0, ua0, {})
-        self.check_balance(0, 0, ua0, {'sapling': 10}, 0)
+        self.check_balance(0, 0, ua0, {'sapling': ua0sapling_balance}, 0)
 
         self.nodes[2].generate(1)
         self.sync_all()
 
         # The default minconf should now detect the balance.
-        self.check_balance(0, 0, ua0, {'sapling': 10})
+        self.check_balance(0, 0, ua0, {'sapling': ua0sapling_balance})
 
         # Send Sapling funds from the UA.
         print('Sending account funds to Sapling address')
         node1sapling = self.nodes[1].z_getnewaddress('sapling')
 
+        fee = conventional_fee(2)
         recipients = [{'address': node1sapling, 'amount': Decimal('1')}]
-        opid = self.nodes[0].z_sendmany(ua0, recipients, 1, 0)
+        opid = self.nodes[0].z_sendmany(ua0, recipients, 1, fee)
         txid = wait_and_assert_operationid_status(self.nodes[0], opid)
+        ua0sapling_balance -= Decimal('1') + fee
 
         # The wallet should detect the spent note as belonging to the UA.
         tx_details = self.nodes[0].z_viewtransaction(txid)
@@ -183,7 +187,7 @@ def run_test(self):
         # up until the transaction expires), or 9 (if we include the unmined transaction).
         self.sync_all()
         self.check_balance(0, 0, ua0, {})
-        self.check_balance(0, 0, ua0, {'sapling': 9}, 0)
+        self.check_balance(0, 0, ua0, {'sapling': ua0sapling_balance}, 0)
 
         # Activate NU5
         print('Activating NU5')
@@ -193,9 +197,11 @@ def run_test(self):
 
         # Send more coinbase funds to the UA.
         print('Sending coinbase funds to account')
-        recipients = [{'address': ua0, 'amount': Decimal('10')}]
-        opid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, 0, 'AllowRevealedSenders')
+        fee = conventional_fee(3)
+        recipients = [{'address': ua0, 'amount': Decimal('10') - fee}]
+        opid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, fee, 'AllowRevealedSenders')
         txid = wait_and_assert_operationid_status(self.nodes[0], opid)
+        ua0orchard_balance = Decimal('10') - fee
 
         # The wallet should detect the new note as belonging to the UA.
         tx_details = self.nodes[0].z_viewtransaction(txid)
@@ -206,12 +212,12 @@ def run_test(self):
         # The new balance should not be visible with the default minconf, but should be
         # visible with minconf=0.
         self.sync_all()
-        self.check_balance(0, 0, ua0, {'sapling': 9})
-        self.check_balance(0, 0, ua0, {'sapling': 9, 'orchard': 10}, 0)
+        self.check_balance(0, 0, ua0, {'sapling': ua0sapling_balance})
+        self.check_balance(0, 0, ua0, {'sapling': ua0sapling_balance, 'orchard': ua0orchard_balance}, 0)
 
         # The total balance with the default minconf should be just the Sapling balance
-        assert_equal('9.00', self.nodes[0].z_gettotalbalance()['private'])
-        assert_equal('19.00', self.nodes[0].z_gettotalbalance(0)['private'])
+        assert_equal(Decimal(self.nodes[0].z_gettotalbalance()['private']), ua0sapling_balance)
+        assert_equal(Decimal(self.nodes[0].z_gettotalbalance(0)['private']), ua0sapling_balance + ua0orchard_balance)
 
         self.nodes[2].generate(1)
         self.sync_all()
@@ -223,9 +229,11 @@ def run_test(self):
         self.check_z_listaccounts(1, 0, 0, node1orchard)
         node1orchard = node1orchard['address']
 
+        fee = conventional_fee(2)
         recipients = [{'address': node1orchard, 'amount': Decimal('1')}]
-        opid = self.nodes[0].z_sendmany(ua0, recipients, 1, 0)
+        opid = self.nodes[0].z_sendmany(ua0, recipients, 1, fee)
         txid = wait_and_assert_operationid_status(self.nodes[0], opid)
+        ua0orchard_balance -= Decimal('1') + fee
 
         # The wallet should detect the spent note as belonging to the UA.
         tx_details = self.nodes[0].z_viewtransaction(txid)
@@ -248,8 +256,8 @@ def run_test(self):
         # that transaction has been created and broadcast, and _might_ get mined up until
         # the transaction expires), or 9 (if we include the unmined transaction).
         self.sync_all()
-        self.check_balance(0, 0, ua0, {'sapling': 9})
-        self.check_balance(0, 0, ua0, {'sapling': 9, 'orchard': 9}, 0)
+        self.check_balance(0, 0, ua0, {'sapling': ua0sapling_balance})
+        self.check_balance(0, 0, ua0, {'sapling': ua0sapling_balance, 'orchard': ua0orchard_balance}, 0)
 
 
 if __name__ == '__main__':
diff --git a/qa/rpc-tests/wallet_anchorfork.py b/qa/rpc-tests/wallet_anchorfork.py
index d8bd8cda5e0..3009d7dab8f 100755
--- a/qa/rpc-tests/wallet_anchorfork.py
+++ b/qa/rpc-tests/wallet_anchorfork.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright (c) 2018 The Zcash developers
+# Copyright (c) 2018-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
@@ -7,7 +7,9 @@
 from test_framework.util import assert_equal, initialize_chain_clean, \
     start_nodes, stop_nodes, connect_nodes_bi, \
     wait_and_assert_operationid_status, wait_bitcoinds, get_coinbase_address, \
-    sync_blocks, sync_mempools, LEGACY_DEFAULT_FEE
+    sync_blocks, sync_mempools
+from test_framework.zip317 import conventional_fee
+
 from decimal import Decimal
 
 class WalletAnchorForkTest (BitcoinTestFramework):
@@ -51,9 +53,9 @@ def run_test (self):
         # Node 0 creates a joinsplit transaction
         mytaddr0 = get_coinbase_address(self.nodes[0])
         myzaddr0 = self.nodes[0].z_getnewaddress()
-        recipients = []
-        recipients.append({"address":myzaddr0, "amount": Decimal('10.0') - LEGACY_DEFAULT_FEE})
-        myopid = self.nodes[0].z_sendmany(mytaddr0, recipients, 1, LEGACY_DEFAULT_FEE, 'AllowRevealedSenders')
+        fee = conventional_fee(3)
+        recipients = [{"address": myzaddr0, "amount": Decimal('10.0') - fee}]
+        myopid = self.nodes[0].z_sendmany(mytaddr0, recipients, 1, fee, 'AllowRevealedSenders')
         wait_and_assert_operationid_status(self.nodes[0], myopid)
 
         # Sync up mempools and mine the transaction.  All nodes have the same anchor.
@@ -75,9 +77,9 @@ def run_test (self):
         self.nodes[1].generate(1)
 
         # Partition A, node 0 creates a joinsplit transaction
-        recipients = []
-        recipients.append({"address":myzaddr0, "amount": Decimal('10.0') - LEGACY_DEFAULT_FEE})
-        myopid = self.nodes[0].z_sendmany(mytaddr0, recipients, 1, LEGACY_DEFAULT_FEE, 'AllowRevealedSenders')
+        fee = conventional_fee(3)
+        recipients = [{"address": myzaddr0, "amount": Decimal('10.0') - fee}]
+        myopid = self.nodes[0].z_sendmany(mytaddr0, recipients, 1, fee, 'AllowRevealedSenders')
         txid = wait_and_assert_operationid_status(self.nodes[0], myopid)
         rawhex = self.nodes[0].getrawtransaction(txid)
 
diff --git a/qa/rpc-tests/wallet_changeaddresses.py b/qa/rpc-tests/wallet_changeaddresses.py
index 4f8e12ae8ab..95a0119b3f3 100755
--- a/qa/rpc-tests/wallet_changeaddresses.py
+++ b/qa/rpc-tests/wallet_changeaddresses.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright (c) 2019 The Zcash developers
+# Copyright (c) 2019-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
@@ -13,6 +13,7 @@
     start_node,
     wait_and_assert_operationid_status,
 )
+from test_framework.zip317 import conventional_fee, ZIP_317_FEE
 
 from decimal import Decimal
 
@@ -25,7 +26,6 @@ def setup_chain(self):
 
     def setup_network(self):
         args = [
-            '-minrelaytxfee=0',
             nuparams(SAPLING_BRANCH_ID, 1),
             '-txindex',              # Avoid JSONRPC error: No information available about transaction
             '-allowdeprecated=getnewaddress',
@@ -43,30 +43,32 @@ def run_test(self):
 
         # Obtain some transparent funds
         midAddr = self.nodes[0].z_getnewaddress('sapling')
-        myopid = self.nodes[0].z_shieldcoinbase(get_coinbase_address(self.nodes[0]), midAddr, 0)['opid']
+        coinbase_fee = conventional_fee(12)
+        myopid = self.nodes[0].z_shieldcoinbase(get_coinbase_address(self.nodes[0]), midAddr, coinbase_fee)['opid']
         wait_and_assert_operationid_status(self.nodes[0], myopid)
 
         self.sync_all()
         self.nodes[1].generate(1)
         self.sync_all()
+
         taddrSource = self.nodes[0].getnewaddress()
+        recipients = [{"address": taddrSource, "amount": Decimal('2')}]
         for _ in range(6):
-            recipients = [{"address": taddrSource, "amount": Decimal('2')}]
-            myopid = self.nodes[0].z_sendmany(midAddr, recipients, 1, 0, 'AllowRevealedRecipients')
+            myopid = self.nodes[0].z_sendmany(midAddr, recipients, 1, ZIP_317_FEE, 'AllowRevealedRecipients')
             wait_and_assert_operationid_status(self.nodes[0], myopid)
             self.sync_all()
             self.nodes[1].generate(1)
             self.sync_all()
 
-        def check_change_taddr_reuse(target, policy):
+        def check_change_taddr_reuse(target, fee, policy):
             recipients = [{"address": target, "amount": Decimal('1')}]
 
             # Send funds to recipient address twice
-            myopid = self.nodes[0].z_sendmany(taddrSource, recipients, 1, 0, policy)
+            myopid = self.nodes[0].z_sendmany(taddrSource, recipients, 1, fee, policy)
             txid1 = wait_and_assert_operationid_status(self.nodes[0], myopid)
             self.nodes[1].generate(1)
             self.sync_all()
-            myopid = self.nodes[0].z_sendmany(taddrSource, recipients, 1, 0, policy)
+            myopid = self.nodes[0].z_sendmany(taddrSource, recipients, 1, fee, policy)
             txid2 = wait_and_assert_operationid_status(self.nodes[0], myopid)
             self.nodes[1].generate(1)
             self.sync_all()
@@ -88,10 +90,10 @@ def check_change_taddr_reuse(target, policy):
 
         print()
         print('Checking z_sendmany(taddr->Sapling)')
-        check_change_taddr_reuse(saplingAddr, 'AllowFullyTransparent')
+        check_change_taddr_reuse(saplingAddr, conventional_fee(3), 'AllowFullyTransparent')
         print()
         print('Checking z_sendmany(taddr->taddr)')
-        check_change_taddr_reuse(taddr, 'AllowFullyTransparent')
+        check_change_taddr_reuse(taddr, conventional_fee(1), 'AllowFullyTransparent')
 
 if __name__ == '__main__':
     WalletChangeAddressesTest().main()
diff --git a/qa/rpc-tests/wallet_changeindicator.py b/qa/rpc-tests/wallet_changeindicator.py
index bf289ac8601..c3a9811bd90 100755
--- a/qa/rpc-tests/wallet_changeindicator.py
+++ b/qa/rpc-tests/wallet_changeindicator.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright (c) 2018 The Zcash developers
+# Copyright (c) 2018-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
@@ -11,6 +11,7 @@
     start_nodes,
     wait_and_assert_operationid_status,
 )
+from test_framework.zip317 import conventional_fee
 
 from decimal import Decimal
 
@@ -23,7 +24,6 @@ def generate_and_sync(self):
 
     def setup_nodes(self):
         return start_nodes(self.num_nodes, self.options.tmpdir, extra_args=[[
-            '-minrelaytxfee=0',
             '-allowdeprecated=getnewaddress',
             '-allowdeprecated=z_getnewaddress',
         ]] * self.num_nodes)
@@ -34,16 +34,16 @@ def run_test(self):
         zaddr1 = self.nodes[1].z_getnewaddress()
         zaddr2 = self.nodes[1].z_getnewaddress()
 
-        self.nodes[0].sendtoaddress(taddr, Decimal('1.0'))
+        fee = conventional_fee(3)
+        self.nodes[0].sendtoaddress(taddr, Decimal('1.0') + fee)
         self.generate_and_sync()
 
         # Send 1 ZEC to a zaddr
-        wait_and_assert_operationid_status(
-            self.nodes[1],
-            self.nodes[1].z_sendmany(
-                taddr,
-                [{'address': zaddr1, 'amount': 1.0, 'memo': 'c0ffee01'}],
-                1, 0, 'AllowRevealedSenders'))
+        opid = self.nodes[1].z_sendmany(taddr,
+                [{'address': zaddr1, 'amount': Decimal('1.0'), 'memo': 'c0ffee01'}],
+                1, fee, 'AllowRevealedSenders')
+        wait_and_assert_operationid_status(self.nodes[1], opid)
+
         self.generate_and_sync()
 
         # Check that we have received 1 note which is not change
@@ -55,14 +55,17 @@ def run_test(self):
         assert_false(listunspent[0]['change'], "Unspent note should not be change")
 
         # Generate some change
-        wait_and_assert_operationid_status(self.nodes[1], self.nodes[1].z_sendmany(zaddr1, [{'address': zaddr2, 'amount': 0.6, 'memo': 'c0ffee02'}], 1, 0))
+        fee = conventional_fee(2)
+        recipients = [{'address': zaddr2, 'amount': Decimal('0.6'), 'memo': 'c0ffee02'}]
+        opid = self.nodes[1].z_sendmany(zaddr1, recipients, 1, fee)
+        wait_and_assert_operationid_status(self.nodes[1], opid)
         self.generate_and_sync()
 
         # Check zaddr1 received
         sortedreceived1 = sorted(self.nodes[1].z_listreceivedbyaddress(zaddr1, 0), key = lambda received: received['amount'])
         assert_equal(2, len(sortedreceived1), "zaddr1 Should have received 2 notes")
-        assert_equal(Decimal('0.4'), sortedreceived1[0]['amount'])
-        assert_true(sortedreceived1[0]['change'], "Note valued at 0.4 should be change")
+        assert_equal(Decimal('0.4') - fee, sortedreceived1[0]['amount'])
+        assert_true(sortedreceived1[0]['change'], "Note valued at 0.4 - fee should be change")
         assert_equal(Decimal('1.0'), sortedreceived1[1]['amount'])
         assert_false(sortedreceived1[1]['change'], "Note valued at 1.0 should not be change")
         # Check zaddr2 received
@@ -73,8 +76,8 @@ def run_test(self):
         # Check unspent
         sortedunspent = sorted(self.nodes[1].z_listunspent(), key = lambda received: received['amount'])
         assert_equal(2, len(sortedunspent), "Should have 2 unspent notes")
-        assert_equal(Decimal('0.4'), sortedunspent[0]['amount'])
-        assert_true(sortedunspent[0]['change'], "Unspent note valued at 0.4 should be change")
+        assert_equal(Decimal('0.4') - fee, sortedunspent[0]['amount'])
+        assert_true(sortedunspent[0]['change'], "Unspent note valued at 0.4 - fee should be change")
         assert_equal(Decimal('0.6'), sortedunspent[1]['amount'])
         assert_false(sortedunspent[1]['change'], "Unspent note valued at 0.6 should not be change")
 
diff --git a/qa/rpc-tests/wallet_doublespend.py b/qa/rpc-tests/wallet_doublespend.py
index 37172637844..97c615ec40a 100755
--- a/qa/rpc-tests/wallet_doublespend.py
+++ b/qa/rpc-tests/wallet_doublespend.py
@@ -1,10 +1,11 @@
 #!/usr/bin/env python3
-# Copyright (c) 2022 The Zcash developers
+# Copyright (c) 2022-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
 import shutil
 import os.path
+from decimal import Decimal
 
 from test_framework.mininode import COIN
 from test_framework.test_framework import BitcoinTestFramework
@@ -18,6 +19,7 @@
     wait_bitcoinds,
     wait_and_assert_operationid_status,
 )
+from test_framework.zip317 import conventional_fee
 
 # Test wallet behaviour with the Orchard protocol
 class WalletDoubleSpendTest(BitcoinTestFramework):
@@ -27,7 +29,6 @@ def __init__(self):
 
     def setup_nodes(self):
         return start_nodes(self.num_nodes, self.options.tmpdir, extra_args=[[
-            '-minrelaytxfee=0',
             nuparams(NU5_BRANCH_ID, 201),
         ]] * self.num_nodes)
 
@@ -45,11 +46,12 @@ def run_test_for_recipient_type(self, recipient_type):
         ua1 = addrRes1['address']
 
         # Create a note matching recipient_type on node 1
-        original_zec_amount = 10
-        recipients = [{"address": ua1, "amount": original_zec_amount}]
-        original_amount = original_zec_amount * COIN
-        myopid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, 0, 'AllowRevealedSenders')
+        coinbase_fee = conventional_fee(3)
+        original_amount = Decimal('10') - coinbase_fee
+        recipients = [{"address": ua1, "amount": original_amount}]
+        myopid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, coinbase_fee, 'AllowRevealedSenders')
         wait_and_assert_operationid_status(self.nodes[0], myopid)
+        acct1_balance = original_amount
 
         self.sync_all()
         self.nodes[0].generate(1)
@@ -57,7 +59,7 @@ def run_test_for_recipient_type(self, recipient_type):
 
         # Check the value sent to ua1 was received
         assert_equal(
-                {'pools': {recipient_type: {'valueZat': original_amount}}, 'minimum_confirmations': 1},
+                {'pools': {recipient_type: {'valueZat': acct1_balance * COIN}}, 'minimum_confirmations': 1},
                 self.nodes[1].z_getbalanceforaccount(acct1))
 
         # Shut down the nodes
@@ -75,29 +77,30 @@ def run_test_for_recipient_type(self, recipient_type):
 
         # Verify the balance on node 1
         assert_equal(
-                {'pools': {recipient_type: {'valueZat': original_amount}},
+                {'pools': {recipient_type: {'valueZat': acct1_balance * COIN}},
                  'minimum_confirmations': 1},
                 self.nodes[1].z_getbalanceforaccount(acct1))
 
         # Verify the balance on node 2, on the other side of the split
         assert_equal(
-                {'pools': {recipient_type: {'valueZat': original_amount}},
+                {'pools': {recipient_type: {'valueZat': acct1_balance * COIN}},
                  'minimum_confirmations': 1},
                 self.nodes[2].z_getbalanceforaccount(acct1))
 
         # Spend the note from node 1
-        node1_zec_spend_amount = 1
-        recipients = [{"address": ua0a, "amount": node1_zec_spend_amount}]
-        node1_spend_amount = node1_zec_spend_amount * COIN
-        myopid = self.nodes[1].z_sendmany(ua1, recipients, 1, 0)
+        fee = conventional_fee(2)
+        node1_spend_amount = Decimal('1')
+        recipients = [{"address": ua0a, "amount": node1_spend_amount}]
+        myopid = self.nodes[1].z_sendmany(ua1, recipients, 1, fee)
         txa_id = wait_and_assert_operationid_status(self.nodes[1], myopid)
+        acct1_balance_1 = acct1_balance - node1_spend_amount - fee
 
         # Spend the note from node 2
-        node2_zec_spend_amount = 2
-        recipients = [{"address": ua0b, "amount": node2_zec_spend_amount}]
-        node2_spend_amount = node2_zec_spend_amount * COIN
-        myopid = self.nodes[2].z_sendmany(ua1, recipients, 1, 0)
+        node2_spend_amount = Decimal('2')
+        recipients = [{"address": ua0b, "amount": node2_spend_amount}]
+        myopid = self.nodes[2].z_sendmany(ua1, recipients, 1, fee)
         txb_id = wait_and_assert_operationid_status(self.nodes[2], myopid)
+        acct1_balance_2 = acct1_balance - node2_spend_amount - fee
 
         # Mine the conflicting notes in the split
         self.sync_all()
@@ -105,14 +108,14 @@ def run_test_for_recipient_type(self, recipient_type):
         self.nodes[3].generate(10)
         self.sync_all()
 
-        # the remaining balance is visible on both sides of the split
+        # the remaining balance is visible on each side of the split
         assert_equal(
-                {'pools': {recipient_type: {'valueZat': original_amount - node1_spend_amount}},
+                {'pools': {recipient_type: {'valueZat': acct1_balance_1 * COIN}},
                  'minimum_confirmations': 1},
                 self.nodes[1].z_getbalanceforaccount(acct1))
 
         assert_equal(
-                {'pools': {recipient_type: {'valueZat': original_amount - node2_spend_amount}},
+                {'pools': {recipient_type: {'valueZat': acct1_balance_2 * COIN}},
                  'minimum_confirmations': 1},
                 self.nodes[2].z_getbalanceforaccount(acct1))
 
@@ -126,7 +129,7 @@ def run_test_for_recipient_type(self, recipient_type):
 
         # acct0a will have received the transaction; it can't see node 2's send
         assert_equal(
-                {'pools': {recipient_type: {'valueZat': node1_spend_amount}}, 'minimum_confirmations': 1},
+                {'pools': {recipient_type: {'valueZat': node1_spend_amount * COIN}}, 'minimum_confirmations': 1},
                 self.nodes[0].z_getbalanceforaccount(acct0a))
 
         self.join_network()
@@ -141,19 +144,20 @@ def run_test_for_recipient_type(self, recipient_type):
 
         # After the reorg, node 2 wins, so its balance is the consensus for
         # both wallets
+        acct1_balance = acct1_balance_2
         assert_equal(
-                {'pools': {recipient_type: {'valueZat': original_amount - node2_spend_amount}},
+                {'pools': {recipient_type: {'valueZat': acct1_balance * COIN}},
                  'minimum_confirmations': 1},
                 self.nodes[1].z_getbalanceforaccount(acct1))
 
         assert_equal(
-                {'pools': {recipient_type: {'valueZat': original_amount - node2_spend_amount}},
+                {'pools': {recipient_type: {'valueZat': acct1_balance * COIN}},
                  'minimum_confirmations': 1},
                 self.nodes[2].z_getbalanceforaccount(acct1))
 
         # acct0b will have received the transaction
         assert_equal(
-                {'pools': {recipient_type: {'valueZat': node2_spend_amount}},
+                {'pools': {recipient_type: {'valueZat': node2_spend_amount * COIN}},
                  'minimum_confirmations': 1},
                 self.nodes[0].z_getbalanceforaccount(acct0b))
 
diff --git a/qa/rpc-tests/wallet_isfromme.py b/qa/rpc-tests/wallet_isfromme.py
index c38c95be02a..f67948a19c7 100755
--- a/qa/rpc-tests/wallet_isfromme.py
+++ b/qa/rpc-tests/wallet_isfromme.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright (c) 2021 The Zcash developers
+# Copyright (c) 2021-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
@@ -17,6 +17,7 @@
     start_nodes,
     wait_and_assert_operationid_status,
 )
+from test_framework.zip317 import conventional_fee
 
 from decimal import Decimal
 
@@ -26,7 +27,6 @@ def setup_chain(self):
 
     def setup_network(self, split=False):
         self.nodes = start_nodes(1, self.options.tmpdir, extra_args=[[
-            '-minrelaytxfee=0',
             nuparams(OVERWINTER_BRANCH_ID, 1),
             nuparams(SAPLING_BRANCH_ID, 1),
             nuparams(BLOSSOM_BRANCH_ID, 1),
@@ -41,46 +41,37 @@ def run_test (self):
         node = self.nodes[0]
 
         node.generate(101)
-        assert_equal(node.getbalance('', 0), Decimal('6.25'))
+        assert_equal(Decimal(node.getbalance('', 0)), Decimal('6.25'))
 
         coinbase_addr = get_coinbase_address(node)
+        coinbase_fee = conventional_fee(3)
 
         # Send all available funds to a z-address.
         zaddr = node.z_getnewaddress()
-        wait_and_assert_operationid_status(
-            node,
-            node.z_sendmany(
-                coinbase_addr,
-                [
-                    {'address': zaddr, 'amount': Decimal('6.25')},
-                ],
-                0,
-                0,
-                'AllowRevealedSenders',
-            ),
-        )
+        recipients = [{'address': zaddr, 'amount': Decimal('6.25') - coinbase_fee}]
+        opid = node.z_sendmany(coinbase_addr, recipients, 0, coinbase_fee, 'AllowRevealedSenders')
+        wait_and_assert_operationid_status(node, opid)
         self.sync_all()
-        assert_equal(node.getbalance('', 0), 0)
+        assert_equal(Decimal(node.getbalance('', 0)), Decimal('0'))
 
         # Mine the transaction; we get another coinbase output.
         self.nodes[0].generate(1)
         self.sync_all()
-        assert_equal(node.getbalance('', 0), Decimal('6.25'))
+        assert_equal(Decimal(node.getbalance('', 0)), Decimal('6.25'))
 
         # Now send the funds back to a new t-address.
         taddr = node.getnewaddress()
-        wait_and_assert_operationid_status(
-            node,
-            node.z_sendmany(
-                zaddr,
-                [
-                    {'address': taddr, 'amount': Decimal('6.25')},
-                ],
-                1,
-                0,
-                'AllowRevealedRecipients',
-            ),
-        )
+        fee = conventional_fee(3)
+        recipients = [{'address': taddr, 'amount': Decimal('6.25') - coinbase_fee - fee}]
+        # TODO: this fails for ZIP_317_FEE due to a dust threshold problem:
+        # "Insufficient funds: have 6.24985, need 0.00000054 more to surpass the dust
+        # threshold and avoid being forced to over-pay the fee. Alternatively, you could
+        # specify a fee of 0.0001 to allow overpayment of the conventional fee and have
+        # this transaction proceed.; note that coinbase outputs will not be selected if
+        # you specify ANY_TADDR, any transparent recipients are included, or if the
+        # `privacyPolicy` parameter is not set to `AllowRevealedSenders` or weaker."
+        opid = node.z_sendmany(zaddr, recipients, 1, fee, 'AllowRevealedRecipients')
+        wait_and_assert_operationid_status(node, opid)
         self.sync_all()
 
         # At this point we have created the conditions for the bug in
@@ -92,7 +83,7 @@ def run_test (self):
         assert_equal(len(node.listunspent(0)), 2)
 
         # "getbalance '' 0" should count both outputs. The bug failed here.
-        assert_equal(node.getbalance('', 0), Decimal('12.5'))
+        assert_equal(Decimal(node.getbalance('', 0)), Decimal('12.5') - coinbase_fee - fee)
 
 if __name__ == '__main__':
     WalletIsFromMe().main ()
diff --git a/qa/rpc-tests/wallet_listnotes.py b/qa/rpc-tests/wallet_listnotes.py
index aafd0cf26b9..7c4eeb121c1 100755
--- a/qa/rpc-tests/wallet_listnotes.py
+++ b/qa/rpc-tests/wallet_listnotes.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright (c) 2018 The Zcash developers
+# Copyright (c) 2018-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
@@ -12,8 +12,8 @@
     nuparams,
     start_nodes,
     wait_and_assert_operationid_status,
-    LEGACY_DEFAULT_FEE
 )
+from test_framework.zip317 import conventional_fee
 
 from decimal import Decimal
 
@@ -25,7 +25,6 @@ def __init__(self):
 
     def setup_nodes(self):
         return start_nodes(4, self.options.tmpdir, extra_args=[[
-            '-minrelaytxfee=0',
             nuparams(NU5_BRANCH_ID, 215),
             '-allowdeprecated=z_getnewaddress',
             '-allowdeprecated=z_getbalance',
@@ -46,10 +45,11 @@ def run_test(self):
         # Send 1.0 minus default fee from sproutzaddr to a new Sapling zaddr
         saplingzaddr = self.nodes[0].z_getnewaddress('sapling')
         receive_amount_2 = Decimal('1.0')
-        change_amount_2 = receive_amount_1 - receive_amount_2 - LEGACY_DEFAULT_FEE
+        fee = conventional_fee(4)
+        change_amount_2 = receive_amount_1 - receive_amount_2 - fee
         assert_equal('sapling', self.nodes[0].z_validateaddress(saplingzaddr)['address_type'])
         recipients = [{"address": saplingzaddr, "amount":receive_amount_2}]
-        myopid = self.nodes[0].z_sendmany(sproutzaddr, recipients, 1, LEGACY_DEFAULT_FEE, 'AllowRevealedAmounts')
+        myopid = self.nodes[0].z_sendmany(sproutzaddr, recipients, 1, fee, 'AllowRevealedAmounts')
         txid_2 = wait_and_assert_operationid_status(self.nodes[0], myopid)
         self.sync_all()
 
@@ -86,9 +86,10 @@ def run_test(self):
         # Send 2.0 minus default fee to a new sapling zaddr
         saplingzaddr2 = self.nodes[0].z_getnewaddress('sapling')
         receive_amount_3 = Decimal('2.0')
-        change_amount_3 = change_amount_2 - receive_amount_3 - LEGACY_DEFAULT_FEE
+        fee = conventional_fee(4)
+        change_amount_3 = change_amount_2 - receive_amount_3 - fee
         recipients = [{"address": saplingzaddr2, "amount":receive_amount_3}]
-        myopid = self.nodes[0].z_sendmany(sproutzaddr, recipients, 1, LEGACY_DEFAULT_FEE, 'AllowRevealedAmounts')
+        myopid = self.nodes[0].z_sendmany(sproutzaddr, recipients, 1, fee, 'AllowRevealedAmounts')
         txid_3 = wait_and_assert_operationid_status(self.nodes[0], myopid)
         self.sync_all()
         unspent_tx = self.nodes[0].z_listunspent(0)
@@ -146,9 +147,10 @@ def run_test(self):
         # Create an Orchard note.
         account0 = self.nodes[0].z_getnewaccount()['account']
         ua0 = self.nodes[0].z_getaddressforaccount(account0)['address']
-        receive_amount_4 = Decimal('10.0')
+        fee = conventional_fee(3)
+        receive_amount_4 = Decimal('10.0') - fee
         recipients = [{"address": ua0, "amount": receive_amount_4}]
-        myopid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, 0, 'AllowRevealedSenders')
+        myopid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, fee, 'AllowRevealedSenders')
         txid_4 = wait_and_assert_operationid_status(self.nodes[0], myopid)
         self.sync_all()
 
diff --git a/qa/rpc-tests/wallet_listreceived.py b/qa/rpc-tests/wallet_listreceived.py
index f3b685c5731..2bc06db97d0 100755
--- a/qa/rpc-tests/wallet_listreceived.py
+++ b/qa/rpc-tests/wallet_listreceived.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright (c) 2018 The Zcash developers
+# Copyright (c) 2018-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
@@ -32,7 +32,6 @@ def __init__(self):
 
     def setup_network(self):
         self.nodes = start_nodes(self.num_nodes, self.options.tmpdir, extra_args=[[
-            '-minrelaytxfee=0',
             nuparams(NU5_BRANCH_ID, 225),
             '-allowdeprecated=getnewaddress',
             '-allowdeprecated=z_getnewaddress',
@@ -304,7 +303,7 @@ def test_received_orchard(self, height):
         opid = self.nodes[1].z_sendmany(taddr, [
             {'address': uao, 'amount': 1, 'memo': my_memo},
             {'address': uaso, 'amount': 2},
-        ], 1, 0, 'AllowFullyTransparent')
+        ], 1, ZIP_317_FEE, 'AllowFullyTransparent')
         txid0 = wait_and_assert_operationid_status(self.nodes[1], opid)
         self.sync_all()
 
diff --git a/qa/rpc-tests/wallet_listunspent.py b/qa/rpc-tests/wallet_listunspent.py
index c0c2255f9d3..42291ae5362 100755
--- a/qa/rpc-tests/wallet_listunspent.py
+++ b/qa/rpc-tests/wallet_listunspent.py
@@ -1,9 +1,10 @@
 #!/usr/bin/env python3
-# Copyright (c) 2016-2022 The Zcash developers
+# Copyright (c) 2016-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
 from test_framework.test_framework import BitcoinTestFramework
+from test_framework.mininode import COIN
 from test_framework.util import (
     assert_equal,
     get_coinbase_address,
@@ -12,6 +13,7 @@
     wait_and_assert_operationid_status,
     NU5_BRANCH_ID
 )
+from test_framework.zip317 import conventional_fee
 
 from decimal import Decimal
 
@@ -21,7 +23,6 @@ def unspent_total(unspent):
 class WalletListUnspent(BitcoinTestFramework):
     def setup_nodes(self):
         return start_nodes(4, self.options.tmpdir, extra_args=[[
-            '-minrelaytxfee=0',
             nuparams(NU5_BRANCH_ID, 201),
             '-allowdeprecated=getnewaddress',
         ]] * 4)
@@ -33,24 +34,23 @@ def run_test(self):
         def expected_matured_at_height(height):
             return (height-200)*10 + 250
 
-        assert_equal(self.nodes[0].getbalance(), expected_matured_at_height(200))
-        assert_equal(self.nodes[1].getbalance(), expected_matured_at_height(200))
+        assert_equal(Decimal(self.nodes[0].getbalance()), expected_matured_at_height(200))
+        assert_equal(Decimal(self.nodes[1].getbalance()), expected_matured_at_height(200))
 
         # Activate NU5
         self.nodes[1].generate(1) # height 201
         self.sync_all()
-        assert_equal(self.nodes[0].getbalance(), expected_matured_at_height(201))
+        assert_equal(Decimal(self.nodes[0].getbalance()), expected_matured_at_height(201))
         # check balances from before the latest tx
-        assert_equal(self.nodes[0].getbalance("", 1, False, False, 200), expected_matured_at_height(200))
+        assert_equal(Decimal(self.nodes[0].getbalance("", 1, False, False, 200)), expected_matured_at_height(200))
         assert_equal(self.matured_at_height(200), expected_matured_at_height(200))
 
         # Shield some coinbase funds so that they become spendable
         n1acct = self.nodes[1].z_getnewaccount()['account']
         n1uaddr = self.nodes[1].z_getaddressforaccount(n1acct)['address']
-        opid = self.nodes[0].z_sendmany(
-                get_coinbase_address(self.nodes[0]),
-                [{'address': n1uaddr, 'amount': 10}],
-                1, 0, 'AllowRevealedSenders')
+        fee = conventional_fee(3)
+        recipients = [{'address': n1uaddr, 'amount': Decimal('10') - fee}]
+        opid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, fee, 'AllowRevealedSenders')
         wait_and_assert_operationid_status(self.nodes[0], opid)
 
         self.sync_all()
@@ -60,60 +60,52 @@ def expected_matured_at_height(height):
 
         assert_equal(
                 self.nodes[1].z_getbalanceforaccount(n1acct, 1)['pools']['orchard']['valueZat'],
-                Decimal('1000000000'))
+                (Decimal('10') - fee) * COIN)
 
         # Get a bare legacy transparent address for node 0
         n0addr = self.nodes[0].getnewaddress()
 
         # Send funds to the node 0 address so we have transparent funds to spend.
-        opid = self.nodes[1].z_sendmany(
-                n1uaddr,
-                [{'address': n0addr, 'amount': 10}],
-                1, 0, 'AllowRevealedRecipients')
+        recipients = [{'address': n0addr, 'amount': Decimal('10') - 2 * fee}]
+        opid = self.nodes[1].z_sendmany(n1uaddr, recipients, 1, fee, 'AllowRevealedRecipients')
         wait_and_assert_operationid_status(self.nodes[1], opid)
 
         self.sync_all()
         self.nodes[1].generate(2)
         self.sync_all() # height 205
-        assert_equal(self.nodes[0].getbalance(), expected_matured_at_height(205) - 10 + 10)
+        assert_equal(Decimal(self.nodes[0].getbalance()), expected_matured_at_height(205) - 10 + 10 - 2 * fee)
 
         # We will then perform several spends, and then check the list of
         # unspent notes as of various heights.
 
-        opid = self.nodes[0].z_sendmany(
-                'ANY_TADDR',
-                [{'address': n1uaddr, 'amount': 2}],
-                1, 0, 'AllowFullyTransparent')
+        recipients = [{'address': n1uaddr, 'amount': Decimal('2')}]
+        opid = self.nodes[0].z_sendmany('ANY_TADDR', recipients, 1, fee, 'AllowFullyTransparent')
         wait_and_assert_operationid_status(self.nodes[0], opid)
 
         self.nodes[0].generate(2)
         self.sync_all() # height 207
-        assert_equal(self.nodes[0].getbalance(), expected_matured_at_height(207) - 10 + 10 - 2)
+        assert_equal(Decimal(self.nodes[0].getbalance()), expected_matured_at_height(207) - 10 + 10 - 2 - 3 * fee)
 
-        opid = self.nodes[0].z_sendmany(
-                'ANY_TADDR',
-                [{'address': n1uaddr, 'amount': 3}],
-                1, 0, 'AllowFullyTransparent')
+        recipients = [{'address': n1uaddr, 'amount': Decimal('3')}]
+        opid = self.nodes[0].z_sendmany('ANY_TADDR', recipients, 1, fee, 'AllowFullyTransparent')
         wait_and_assert_operationid_status(self.nodes[0], opid)
 
         self.nodes[0].generate(2)
         self.sync_all() # height 209
-        assert_equal(self.nodes[0].getbalance(), expected_matured_at_height(209) - 10 + 10 - 2 - 3)
+        assert_equal(Decimal(self.nodes[0].getbalance()), expected_matured_at_height(209) - 10 + 10 - 2 - 3 - 4 * fee)
 
-        opid = self.nodes[0].z_sendmany(
-                'ANY_TADDR',
-                [{'address': n1uaddr, 'amount': 5}],
-                1, 0, 'AllowRevealedSenders')
+        recipients = [{'address': n1uaddr, 'amount': Decimal('5') - 5 * fee}]
+        opid = self.nodes[0].z_sendmany('ANY_TADDR', recipients, 1, fee, 'AllowRevealedSenders')
         wait_and_assert_operationid_status(self.nodes[0], opid)
 
         self.nodes[0].generate(2)
         self.sync_all() # height 211
-        assert_equal(self.nodes[0].getbalance(), expected_matured_at_height(211) - 10 + 10 - 2 - 3 - 5)
+        assert_equal(Decimal(self.nodes[0].getbalance()), expected_matured_at_height(211) - 10 + 10 - 2 - 3 - 5)
 
         # check balances at various past points in the chain
-        assert_equal(self.matured_at_height(205), expected_matured_at_height(205) - 10 + 10)
-        assert_equal(self.matured_at_height(207), expected_matured_at_height(207) - 10 + 10 - 2)
-        assert_equal(self.matured_at_height(209), expected_matured_at_height(209) - 10 + 10 - 2 - 3)
+        assert_equal(self.matured_at_height(205), expected_matured_at_height(205) - 10 + 10 - 2 * fee)
+        assert_equal(self.matured_at_height(207), expected_matured_at_height(207) - 10 + 10 - 2 - 3 * fee)
+        assert_equal(self.matured_at_height(209), expected_matured_at_height(209) - 10 + 10 - 2 - 3 - 4 * fee)
         assert_equal(self.matured_at_height(211), expected_matured_at_height(211) - 10 + 10 - 2 - 3 - 5)
 
 if __name__ == '__main__':
diff --git a/qa/rpc-tests/wallet_nullifiers.py b/qa/rpc-tests/wallet_nullifiers.py
index 3462517092a..6bc1f832f38 100755
--- a/qa/rpc-tests/wallet_nullifiers.py
+++ b/qa/rpc-tests/wallet_nullifiers.py
@@ -1,13 +1,13 @@
 #!/usr/bin/env python3
-# Copyright (c) 2016 The Zcash developers
+# Copyright (c) 2016-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
 from test_framework.test_framework import BitcoinTestFramework
 from test_framework.util import assert_equal, assert_true, bitcoind_processes, \
     connect_nodes_bi, start_node, start_nodes, wait_and_assert_operationid_status, \
-    get_coinbase_address, LEGACY_DEFAULT_FEE
-from test_framework.zip317 import conventional_fee
+    get_coinbase_address
+from test_framework.zip317 import conventional_fee, ZIP_317_FEE
 
 from decimal import Decimal
 
@@ -31,12 +31,13 @@ def run_test (self):
         # send node 0 taddr to zaddr to get out of coinbase
         # Tests using the default cached chain have one address per coinbase output
         mytaddr = get_coinbase_address(self.nodes[0])
+        coinbase_fee = conventional_fee(3)
         recipients = []
-        recipients.append({"address": myzaddr0, "amount": Decimal('10.0') - LEGACY_DEFAULT_FEE}) # utxo amount less fee
+        recipients.append({"address": myzaddr0, "amount": Decimal('10.0') - coinbase_fee}) # utxo amount less fee
 
         wait_and_assert_operationid_status(
             self.nodes[0],
-            self.nodes[0].z_sendmany(mytaddr, recipients, 1, LEGACY_DEFAULT_FEE, 'AllowRevealedSenders'),
+            self.nodes[0].z_sendmany(mytaddr, recipients, 1, coinbase_fee, 'AllowRevealedSenders'),
             timeout=120)
 
         self.sync_all()
@@ -109,7 +110,7 @@ def run_test (self):
 
         wait_and_assert_operationid_status(
             self.nodes[1],
-            self.nodes[1].z_sendmany(myzaddr, recipients, 1, LEGACY_DEFAULT_FEE, 'AllowRevealedRecipients'),
+            self.nodes[1].z_sendmany(myzaddr, recipients, 1, ZIP_317_FEE, 'AllowRevealedRecipients'),
             timeout=120)
 
         self.sync_all()
@@ -118,7 +119,7 @@ def run_test (self):
 
         # check zaddr balance
         zsendmany3notevalue = Decimal('1.0')
-        zaddrremaining2 = zaddrremaining - zsendmany3notevalue - LEGACY_DEFAULT_FEE
+        zaddrremaining2 = zaddrremaining - zsendmany3notevalue - conventional_fee(3)
         assert_equal(self.nodes[1].z_getbalance(myzaddr), zaddrremaining2)
         assert_equal(self.nodes[2].z_getbalance(myzaddr), zaddrremaining2)
 
diff --git a/qa/rpc-tests/wallet_orchard.py b/qa/rpc-tests/wallet_orchard.py
index 01c27fcd3fb..0d2889e4270 100755
--- a/qa/rpc-tests/wallet_orchard.py
+++ b/qa/rpc-tests/wallet_orchard.py
@@ -1,9 +1,10 @@
 #!/usr/bin/env python3
-# Copyright (c) 2022 The Zcash developers
+# Copyright (c) 2022-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
 from test_framework.test_framework import BitcoinTestFramework
+from test_framework.mininode import COIN
 from test_framework.util import (
     NU5_BRANCH_ID,
     assert_equal,
@@ -12,6 +13,7 @@
     start_nodes,
     wait_and_assert_operationid_status,
 )
+from test_framework.zip317 import conventional_fee
 
 from decimal import Decimal
 
@@ -23,7 +25,6 @@ def __init__(self):
 
     def setup_nodes(self):
         return start_nodes(self.num_nodes, self.options.tmpdir, extra_args=[[
-            '-minrelaytxfee=0',
             nuparams(NU5_BRANCH_ID, 210),
         ]] * self.num_nodes)
 
@@ -52,9 +53,12 @@ def run_test(self):
         ua2 = addrRes2['address']
         saplingAddr2 = self.nodes[2].z_listunifiedreceivers(ua2)['sapling']
 
-        recipients = [{"address": saplingAddr2, "amount": Decimal('10')}]
-        myopid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, 0, 'AllowRevealedSenders')
+        coinbase_fee = conventional_fee(3)
+        coinbase_amount = Decimal('10') - coinbase_fee
+        recipients = [{"address": saplingAddr2, "amount": coinbase_amount}]
+        myopid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, coinbase_fee, 'AllowRevealedSenders')
         wait_and_assert_operationid_status(self.nodes[0], myopid)
+        acct2_balance = coinbase_amount
 
         # Mine the tx & activate NU5
         self.sync_all()
@@ -63,14 +67,15 @@ def run_test(self):
 
         # Check the value sent to saplingAddr2 was received in node 2's account
         assert_equal(
-                {'pools': {'sapling': {'valueZat': Decimal('1000000000')}}, 'minimum_confirmations': 1},
+                {'pools': {'sapling': {'valueZat': acct2_balance * COIN}}, 'minimum_confirmations': 1},
                 self.nodes[2].z_getbalanceforaccount(acct2))
 
         # Node 0 shields some funds
         # t-coinbase -> Orchard
-        recipients = [{"address": ua1, "amount": Decimal('10')}]
-        myopid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, 0, 'AllowRevealedSenders')
+        recipients = [{"address": ua1, "amount": coinbase_amount}]
+        myopid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, coinbase_fee, 'AllowRevealedSenders')
         mytxid = wait_and_assert_operationid_status(self.nodes[0], myopid)
+        acct1_balance = coinbase_amount
 
         resp = self.nodes[0].getrawtransaction(mytxid, 1)['orchard']
 
@@ -88,8 +93,8 @@ def run_test(self):
         flags = resp['flags']
         assert_equal(flags['enableSpends'], True)
         assert_equal(flags['enableOutputs'], True)
-        assert_equal(resp['valueBalance'], Decimal('-10'))
-        assert_equal(resp['valueBalanceZat'], Decimal('-1000000000'))
+        assert_equal(resp['valueBalance'], -coinbase_amount)
+        assert_equal(resp['valueBalanceZat'], -coinbase_amount * COIN)
         assert('anchor' in resp)
         assert('proof' in resp)
         assert('bindingSig' in resp)
@@ -99,16 +104,17 @@ def run_test(self):
         self.sync_all()
 
         assert_equal(
-                {'pools': {'orchard': {'valueZat': Decimal('1000000000')}}, 'minimum_confirmations': 1},
+                {'pools': {'orchard': {'valueZat': acct1_balance * COIN}}, 'minimum_confirmations': 1},
                 self.nodes[1].z_getbalanceforaccount(acct1))
 
         # Split the network
         self.split_network()
 
-        # Send another tx to ua1
-        recipients = [{"address": ua1, "amount": Decimal('10')}]
-        myopid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, 0, 'AllowRevealedSenders')
+        # Send another coinbase_amount to ua1
+        recipients = [{"address": ua1, "amount": coinbase_amount}]
+        myopid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, coinbase_fee, 'AllowRevealedSenders')
         wait_and_assert_operationid_status(self.nodes[0], myopid)
+        acct1_balance += coinbase_amount
 
         # Mine the tx & generate a majority chain on the 0/1 side of the split
         self.sync_all()
@@ -116,7 +122,7 @@ def run_test(self):
         self.sync_all()
 
         assert_equal(
-                {'pools': {'orchard': {'valueZat': Decimal('2000000000')}}, 'minimum_confirmations': 1},
+                {'pools': {'orchard': {'valueZat': acct1_balance * COIN}}, 'minimum_confirmations': 1},
                 self.nodes[1].z_getbalanceforaccount(acct1))
 
         # On the other side of the split, send some funds to node 3
@@ -125,9 +131,12 @@ def run_test(self):
         assert_equal(acct3, addrRes3['account'])
         ua3 = addrRes3['address']
 
+        fee = conventional_fee(4)
         recipients = [{"address": ua3, "amount": Decimal('1')}]
-        myopid = self.nodes[2].z_sendmany(ua2, recipients, 1, 0, 'AllowRevealedAmounts')
+        myopid = self.nodes[2].z_sendmany(ua2, recipients, 1, fee, 'AllowRevealedAmounts')
         rollback_tx = wait_and_assert_operationid_status(self.nodes[2], myopid)
+        acct3_balance = Decimal('1')
+        acct2_balance -= Decimal('1') + fee
 
         self.sync_all()
         self.nodes[2].generate(1)
@@ -136,11 +145,11 @@ def run_test(self):
         # The remaining change from ua2's Sapling note has been sent to the
         # account's internal Orchard change address.
         assert_equal(
-                {'pools': {'orchard': {'valueZat': Decimal('900000000')}}, 'minimum_confirmations': 1},
+                {'pools': {'orchard': {'valueZat': acct2_balance * COIN}}, 'minimum_confirmations': 1},
                 self.nodes[2].z_getbalanceforaccount(acct2))
 
         assert_equal(
-                {'pools': {'orchard': {'valueZat': Decimal('100000000')}}, 'minimum_confirmations': 1},
+                {'pools': {'orchard': {'valueZat': acct3_balance * COIN}}, 'minimum_confirmations': 1},
                 self.nodes[3].z_getbalanceforaccount(acct3))
 
         # Check that the mempools are empty
@@ -153,7 +162,7 @@ def run_test(self):
 
         # split 0/1's chain should have won, so their wallet balance should be consistent
         assert_equal(
-                {'pools': {'orchard': {'valueZat': Decimal('2000000000')}}, 'minimum_confirmations': 1},
+                {'pools': {'orchard': {'valueZat': acct1_balance * COIN}}, 'minimum_confirmations': 1},
                 self.nodes[1].z_getbalanceforaccount(acct1))
 
         # split 2/3's chain should have been rolled back, so their txn should have been
@@ -166,9 +175,9 @@ def run_test(self):
                 {'pools': {}, 'minimum_confirmations': 1},
                 self.nodes[2].z_getbalanceforaccount(acct2))
 
-        # acct2's incoming change (unconfirmed, still in the mempool) is 9 zec
+        # acct2's incoming change is unconfirmed and still in the mempool
         assert_equal(
-                {'pools': {'orchard': {'valueZat': Decimal('900000000')}}, 'minimum_confirmations': 0},
+                {'pools': {'orchard': {'valueZat': acct2_balance * COIN}}, 'minimum_confirmations': 0},
                 self.nodes[2].z_getbalanceforaccount(acct2, 0))
 
         # The transaction was un-mined, so acct3 should have no confirmed balance
@@ -178,7 +187,7 @@ def run_test(self):
 
         # acct3's unconfirmed balance is 1 zec
         assert_equal(
-                {'pools': {'orchard': {'valueZat': Decimal('100000000')}}, 'minimum_confirmations': 0},
+                {'pools': {'orchard': {'valueZat': acct3_balance * COIN}}, 'minimum_confirmations': 0},
                 self.nodes[3].z_getbalanceforaccount(acct3, 0))
 
         # Manually resend the transaction in node 2's mempool
@@ -191,41 +200,50 @@ def run_test(self):
 
         # The un-mined transaction should now have been re-mined
         assert_equal(
-                {'pools': {'orchard': {'valueZat': Decimal('900000000')}}, 'minimum_confirmations': 1},
+                {'pools': {'orchard': {'valueZat': acct2_balance * COIN}}, 'minimum_confirmations': 1},
                 self.nodes[2].z_getbalanceforaccount(acct2))
 
         assert_equal(
-                {'pools': {'orchard': {'valueZat': Decimal('100000000')}}, 'minimum_confirmations': 1},
+                {'pools': {'orchard': {'valueZat': acct3_balance * COIN}}, 'minimum_confirmations': 1},
                 self.nodes[3].z_getbalanceforaccount(acct3))
 
         # Split the network again
         self.split_network()
 
         # Spend some of acct3's funds on the 2/3 side of the split
-        recipients = [{"address": ua2, "amount": Decimal('0.5')}]
-        myopid = self.nodes[3].z_sendmany(ua3, recipients, 1, 0)
+        fee = conventional_fee(4)
+        amount = Decimal('0.5')
+        recipients = [{"address": ua2, "amount": amount}]
+        myopid = self.nodes[3].z_sendmany(ua3, recipients, 1, fee)
         rollback_tx = wait_and_assert_operationid_status(self.nodes[3], myopid)
+        acct2_balance += amount
+        acct3_balance -= amount + fee
 
         self.sync_all()
         self.nodes[2].generate(1)
         self.sync_all()
 
         assert_equal(
-                {'pools': {'orchard': {'valueZat': Decimal('950000000')}}, 'minimum_confirmations': 1},
+                {'pools': {'orchard': {'valueZat': acct2_balance * COIN}}, 'minimum_confirmations': 1},
                 self.nodes[2].z_getbalanceforaccount(acct2))
 
+        assert_equal(
+                {'pools': {'orchard': {'valueZat': acct3_balance * COIN}}, 'minimum_confirmations': 1},
+                self.nodes[3].z_getbalanceforaccount(acct3))
+
         # Generate a majority chain on the 0/1 side of the split, then 
         # re-join the network.
         self.nodes[1].generate(10)
         self.join_network()
+        acct2_balance -= amount
 
         # split 2/3's chain should have been rolled back, so their txn should have been
         # un-mined and returned to the mempool
         assert_equal(set([rollback_tx]), set(self.nodes[3].getrawmempool()))
 
-        # acct2's balance is back to not contain the Orchard->Orchard value
+        # acct2's balance is back to not containing the Orchard->Orchard value
         assert_equal(
-                {'pools': {'orchard': {'valueZat': Decimal('900000000')}}, 'minimum_confirmations': 1},
+                {'pools': {'orchard': {'valueZat': acct2_balance * COIN}}, 'minimum_confirmations': 1},
                 self.nodes[2].z_getbalanceforaccount(acct2))
 
         # acct3's sole Orchard note is spent by a transaction in the mempool, so our
@@ -236,6 +254,7 @@ def run_test(self):
 
         # Manually resend the transaction in node 3's mempool
         self.nodes[2].resendwallettransactions()
+        acct2_balance += amount
 
         # Sync the network
         self.sync_all()
@@ -244,7 +263,7 @@ def run_test(self):
 
         # The un-mined transaction should now have been re-mined
         assert_equal(
-                {'pools': {'orchard': {'valueZat': Decimal('950000000')}}, 'minimum_confirmations': 1},
+                {'pools': {'orchard': {'valueZat': acct2_balance * COIN}}, 'minimum_confirmations': 1},
                 self.nodes[2].z_getbalanceforaccount(acct2))
 
 if __name__ == '__main__':
diff --git a/qa/rpc-tests/wallet_orchard_change.py b/qa/rpc-tests/wallet_orchard_change.py
index fb4487b48ab..a7208c8e132 100755
--- a/qa/rpc-tests/wallet_orchard_change.py
+++ b/qa/rpc-tests/wallet_orchard_change.py
@@ -1,9 +1,12 @@
 #!/usr/bin/env python3
-# Copyright (c) 2022 The Zcash developers
+# Copyright (c) 2022-2024 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.test_framework import BitcoinTestFramework
+from test_framework.mininode import COIN
 from test_framework.util import (
     NU5_BRANCH_ID,
     assert_equal,
@@ -14,6 +17,7 @@
     wait_and_assert_operationid_status,
     wait_bitcoinds,
 )
+from test_framework.zip317 import conventional_fee, ZIP_317_FEE
 
 # Test wallet behaviour with the Orchard protocol
 class WalletOrchardChangeTest(BitcoinTestFramework):
@@ -23,7 +27,6 @@ def __init__(self):
 
     def setup_nodes(self):
         return start_nodes(self.num_nodes, self.options.tmpdir, extra_args=[[
-            '-minrelaytxfee=0',
             nuparams(NU5_BRANCH_ID, 205),
             '-regtestwalletsetbestchaineveryblock',
         ]] * self.num_nodes)
@@ -46,8 +49,10 @@ def run_test(self):
         acct0 = self.nodes[0].z_getnewaccount()['account']
         ua0 = self.nodes[0].z_getaddressforaccount(acct0, ['sapling', 'orchard'])['address']
 
-        recipients = [{"address": ua0, "amount": 10}]
-        myopid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, 0, 'AllowRevealedSenders')
+        coinbase_fee = conventional_fee(3)
+        balance0 = Decimal('10') - coinbase_fee
+        recipients = [{"address": ua0, "amount": balance0}]
+        myopid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, coinbase_fee, 'AllowRevealedSenders')
         wait_and_assert_operationid_status(self.nodes[0], myopid)
 
         # Mine the tx & activate NU5.
@@ -56,7 +61,7 @@ def run_test(self):
         self.sync_all()
 
         assert_equal(
-            {'pools': {'sapling': {'valueZat': 10_0000_0000}}, 'minimum_confirmations': 1},
+            {'pools': {'sapling': {'valueZat': balance0 * COIN}}, 'minimum_confirmations': 1},
             self.nodes[0].z_getbalanceforaccount(acct0),
         )
 
@@ -65,8 +70,12 @@ def run_test(self):
         acct1 = self.nodes[1].z_getnewaccount()['account']
         ua1 = self.nodes[1].z_getaddressforaccount(acct1, ['orchard'])['address']
 
-        recipients = [{"address": ua1, "amount": 1}]
-        myopid = self.nodes[0].z_sendmany(ua0, recipients, 1, 0, 'AllowRevealedAmounts')
+        recipients = [{"address": ua1, "amount": Decimal('1')}]
+        # TODO The z_sendmany call fails when passed `None`/`ZIP_317_FEE` because it calculates
+        # a fee that is too low, so we have to pass in an explicit fee instead.
+        # https://github.com/zcash/zcash/issues/6956
+        fee = conventional_fee(4)
+        myopid = self.nodes[0].z_sendmany(ua0, recipients, 1, fee, 'AllowRevealedAmounts')
         source_tx = wait_and_assert_operationid_status(self.nodes[0], myopid)
 
         self.sync_all()
@@ -74,8 +83,9 @@ def run_test(self):
         self.sync_all()
 
         # The nodes have the expected split of funds.
+        balance0 -= Decimal('1') + fee
         assert_equal(
-            {'pools': {'orchard': {'valueZat': 9_0000_0000}}, 'minimum_confirmations': 1},
+            {'pools': {'orchard': {'valueZat': balance0 * COIN}}, 'minimum_confirmations': 1},
             self.nodes[0].z_getbalanceforaccount(acct0),
         )
         assert_equal(
@@ -88,8 +98,8 @@ def run_test(self):
             'pool': 'orchard',
             'outgoing': False,
             'walletInternal': True,
-            'value': 9,
-            'valueZat': 9_0000_0000,
+            'value': balance0,
+            'valueZat': balance0 * COIN,
         })
 
         # Shut down the nodes, and restart so that we can check wallet load
@@ -99,7 +109,7 @@ def run_test(self):
 
         # The nodes have unaltered balances.
         assert_equal(
-            {'pools': {'orchard': {'valueZat': 9_0000_0000}}, 'minimum_confirmations': 1},
+            {'pools': {'orchard': {'valueZat': balance0 * COIN}}, 'minimum_confirmations': 1},
             self.nodes[0].z_getbalanceforaccount(acct0),
         )
         assert_equal(
@@ -108,15 +118,16 @@ def run_test(self):
         )
 
         # Send another Orchard transaction from node 0 to node 1.
-        myopid = self.nodes[0].z_sendmany(ua0, recipients, 1, 0)
+        myopid = self.nodes[0].z_sendmany(ua0, recipients, 1, ZIP_317_FEE)
         wait_and_assert_operationid_status(self.nodes[0], myopid)
 
         self.sync_all()
         self.nodes[0].generate(1)
         self.sync_all()
 
+        balance0 -= Decimal('1') + conventional_fee(2)
         assert_equal(
-            {'pools': {'orchard': {'valueZat': 8_0000_0000}}, 'minimum_confirmations': 1},
+            {'pools': {'orchard': {'valueZat': balance0 * COIN}}, 'minimum_confirmations': 1},
             self.nodes[0].z_getbalanceforaccount(acct0),
         )
 
diff --git a/qa/rpc-tests/wallet_orchard_init.py b/qa/rpc-tests/wallet_orchard_init.py
index 75d7746f8c0..f75f668041c 100755
--- a/qa/rpc-tests/wallet_orchard_init.py
+++ b/qa/rpc-tests/wallet_orchard_init.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright (c) 2022 The Zcash developers
+# Copyright (c) 2022-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
@@ -9,6 +9,7 @@
 from decimal import Decimal
 
 from test_framework.test_framework import BitcoinTestFramework
+from test_framework.mininode import COIN
 from test_framework.util import (
     NU5_BRANCH_ID,
     assert_equal,
@@ -19,6 +20,7 @@
     wait_bitcoinds,
     wait_and_assert_operationid_status,
 )
+from test_framework.zip317 import conventional_fee, ZIP_317_FEE
 
 # Test wallet behaviour with the Orchard protocol
 class OrchardWalletInitTest(BitcoinTestFramework):
@@ -28,7 +30,6 @@ def __init__(self):
 
     def setup_nodes(self):
         return start_nodes(self.num_nodes, self.options.tmpdir, extra_args=[[
-            '-minrelaytxfee=0',
             nuparams(NU5_BRANCH_ID, 205),
             '-regtestwalletsetbestchaineveryblock'
         ]] * self.num_nodes)
@@ -50,8 +51,10 @@ def run_test(self):
         ua1 = self.nodes[1].z_getaddressforaccount(acct1, ['orchard'])['address']
 
         # Send a transaction to node 1 so that it has an Orchard note to spend.
-        recipients = [{"address": ua1, "amount": 10}]
-        myopid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, 0, 'AllowRevealedSenders')
+        coinbase_fee = conventional_fee(3)
+        balance1 = Decimal('10') - coinbase_fee
+        recipients = [{"address": ua1, "amount": balance1}]
+        myopid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, coinbase_fee, 'AllowRevealedSenders')
         wait_and_assert_operationid_status(self.nodes[0], myopid)
 
         self.sync_all()
@@ -60,12 +63,12 @@ def run_test(self):
 
         # Check the value sent to ua1 was received
         assert_equal(
-                {'pools': {'orchard': {'valueZat': 10_0000_0000}}, 'minimum_confirmations': 1},
+                {'pools': {'orchard': {'valueZat': balance1 * COIN}}, 'minimum_confirmations': 1},
                 self.nodes[1].z_getbalanceforaccount(acct1))
 
         # Create an Orchard spend, so that the note commitment tree root gets altered.
         recipients = [{"address": ua0, "amount": 1}]
-        myopid = self.nodes[1].z_sendmany(ua1, recipients, 1, 0)
+        myopid = self.nodes[1].z_sendmany(ua1, recipients, 1, ZIP_317_FEE)
         wait_and_assert_operationid_status(self.nodes[1], myopid)
 
         self.sync_all()
@@ -73,8 +76,9 @@ def run_test(self):
         self.sync_all()
 
         # Verify the balance on both nodes
+        balance1 -= Decimal('1') + conventional_fee(2)
         assert_equal(
-                {'pools': {'orchard': {'valueZat': 9_0000_0000}}, 'minimum_confirmations': 1},
+                {'pools': {'orchard': {'valueZat': balance1 * COIN}}, 'minimum_confirmations': 1},
                 self.nodes[1].z_getbalanceforaccount(acct1))
 
         assert_equal(
@@ -103,7 +107,7 @@ def run_test(self):
         self.setup_network(True)
 
         assert_equal(
-                {'pools': {'orchard': {'valueZat': 9_0000_0000}}, 'minimum_confirmations': 1},
+                {'pools': {'orchard': {'valueZat': balance1 * COIN}}, 'minimum_confirmations': 1},
                 self.nodes[1].z_getbalanceforaccount(acct1))
 
         # Get a new account with an Orchard UA on node 0
@@ -114,7 +118,7 @@ def run_test(self):
         # the bug causes the state of note commitment tree in the wallet to not match
         # the state of the global note commitment tree.
         recipients = [{"address": ua0new, "amount": 1}]
-        myopid = self.nodes[1].z_sendmany(ua1, recipients, 1, 0)
+        myopid = self.nodes[1].z_sendmany(ua1, recipients, 1, ZIP_317_FEE)
         rollback_tx = wait_and_assert_operationid_status(self.nodes[1], myopid)
 
         self.sync_all()
@@ -140,21 +144,23 @@ def run_test(self):
         self.nodes[0].generate(1)
         self.sync_all()
 
+        balance1 -= Decimal('1') + conventional_fee(2)
         assert_equal(
                 {'pools': {'orchard': {'valueZat': 1_0000_0000}}, 'minimum_confirmations': 1},
                 self.nodes[0].z_getbalanceforaccount(acct0new))
 
         # Spend from the note that was just received
         recipients = [{"address": ua1, "amount": Decimal('0.3')}]
-        myopid = self.nodes[0].z_sendmany(ua0new, recipients, 1, 0)
+        myopid = self.nodes[0].z_sendmany(ua0new, recipients, 1, ZIP_317_FEE)
         wait_and_assert_operationid_status(self.nodes[0], myopid)
 
         self.sync_all()
         self.nodes[0].generate(1)
         self.sync_all()
 
+        balance1 += Decimal('0.3')
         assert_equal(
-                {'pools': {'orchard': {'valueZat': 8_3000_0000}}, 'minimum_confirmations': 1},
+                {'pools': {'orchard': {'valueZat': balance1 * COIN}}, 'minimum_confirmations': 1},
                 self.nodes[1].z_getbalanceforaccount(acct1))
 
 
diff --git a/qa/rpc-tests/wallet_orchard_persistence.py b/qa/rpc-tests/wallet_orchard_persistence.py
index 99470dccb60..d5ced7f5d4d 100755
--- a/qa/rpc-tests/wallet_orchard_persistence.py
+++ b/qa/rpc-tests/wallet_orchard_persistence.py
@@ -1,9 +1,10 @@
 #!/usr/bin/env python3
-# Copyright (c) 2022 The Zcash developers
+# Copyright (c) 2022-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
 from test_framework.test_framework import BitcoinTestFramework
+from test_framework.mininode import COIN
 from test_framework.util import (
     NU5_BRANCH_ID,
     assert_equal,
@@ -14,6 +15,7 @@
     wait_bitcoinds,
     wait_and_assert_operationid_status,
 )
+from test_framework.zip317 import conventional_fee, ZIP_317_FEE
 
 from decimal import Decimal
 
@@ -25,7 +27,6 @@ def __init__(self):
 
     def setup_nodes(self):
         return start_nodes(self.num_nodes, self.options.tmpdir, extra_args=[[
-            '-minrelaytxfee=0',
             nuparams(NU5_BRANCH_ID, 201),
         ]] * self.num_nodes)
 
@@ -41,8 +42,10 @@ def run_test(self):
         acct0 = self.nodes[0].z_getnewaccount()['account']
         ua0 = self.nodes[0].z_getaddressforaccount(acct0, ['sapling', 'orchard'])['address']
 
-        recipients = [{"address": ua0, "amount": 10}]
-        myopid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, 0, 'AllowRevealedSenders')
+        coinbase_fee = conventional_fee(3)
+        balance0 = Decimal('10') - coinbase_fee
+        recipients = [{"address": ua0, "amount": balance0}]
+        myopid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, coinbase_fee, 'AllowRevealedSenders')
         wait_and_assert_operationid_status(self.nodes[0], myopid)
 
         # Mine the tx 
@@ -51,7 +54,7 @@ def run_test(self):
         self.sync_all()
 
         assert_equal(
-                {'pools': {'orchard': {'valueZat': 10_0000_0000}}, 'minimum_confirmations': 1},
+                {'pools': {'orchard': {'valueZat': balance0 * COIN}}, 'minimum_confirmations': 1},
                 self.nodes[0].z_getbalanceforaccount(acct0))
 
         # Send to a new orchard-only unified address
@@ -59,15 +62,16 @@ def run_test(self):
         ua1 = self.nodes[1].z_getaddressforaccount(acct1, ['orchard'])['address']
 
         recipients = [{"address": ua1, "amount": 1}]
-        myopid = self.nodes[0].z_sendmany(ua0, recipients, 1, 0)
+        myopid = self.nodes[0].z_sendmany(ua0, recipients, 1, ZIP_317_FEE)
         wait_and_assert_operationid_status(self.nodes[0], myopid)
 
         self.sync_all()
         self.nodes[0].generate(1)
         self.sync_all()
 
+        balance0 -= Decimal('1') + conventional_fee(2)
         assert_equal(
-                {'pools': {'orchard': {'valueZat': 9_0000_0000}}, 'minimum_confirmations': 1},
+                {'pools': {'orchard': {'valueZat': balance0 * COIN}}, 'minimum_confirmations': 1},
                 self.nodes[0].z_getbalanceforaccount(acct0))
         assert_equal(
                 {'pools': {'orchard': {'valueZat': 1_0000_0000}}, 'minimum_confirmations': 1},
@@ -76,7 +80,7 @@ def run_test(self):
         # Send another Orchard transaction from node 0 back to itself, so that the
         # note commitment tree gets advanced.
         recipients = [{"address": ua0, "amount": 1}]
-        myopid = self.nodes[0].z_sendmany(ua0, recipients, 1, 0)
+        myopid = self.nodes[0].z_sendmany(ua0, recipients, 1, ZIP_317_FEE)
         wait_and_assert_operationid_status(self.nodes[0], myopid)
 
         self.sync_all()
@@ -88,20 +92,22 @@ def run_test(self):
         wait_bitcoinds()
         self.setup_network()
 
+        balance0 -= conventional_fee(2)
         assert_equal(
-                {'pools': {'orchard': {'valueZat': 9_0000_0000}}, 'minimum_confirmations': 1},
+                {'pools': {'orchard': {'valueZat': balance0 * COIN}}, 'minimum_confirmations': 1},
                 self.nodes[0].z_getbalanceforaccount(acct0))
 
         recipients = [{"address": ua0, "amount": Decimal('0.5')}]
-        myopid = self.nodes[1].z_sendmany(ua1, recipients, 1, 0)
+        myopid = self.nodes[1].z_sendmany(ua1, recipients, 1, ZIP_317_FEE)
         wait_and_assert_operationid_status(self.nodes[1], myopid)
 
         self.sync_all()
         self.nodes[0].generate(1)
         self.sync_all()
 
+        balance0 += Decimal('0.5')
         assert_equal(
-                {'pools': {'orchard': {'valueZat': 9_5000_0000}}, 'minimum_confirmations': 1},
+                {'pools': {'orchard': {'valueZat': balance0 * COIN}}, 'minimum_confirmations': 1},
                 self.nodes[0].z_getbalanceforaccount(acct0))
 
 if __name__ == '__main__':
diff --git a/qa/rpc-tests/wallet_orchard_reindex.py b/qa/rpc-tests/wallet_orchard_reindex.py
index 4569957648d..c83a873f562 100755
--- a/qa/rpc-tests/wallet_orchard_reindex.py
+++ b/qa/rpc-tests/wallet_orchard_reindex.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright (c) 2023 The Zcash developers
+# Copyright (c) 2023-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
@@ -16,12 +16,12 @@
     wait_and_assert_operationid_status,
     wait_bitcoinds,
 )
+from test_framework.zip317 import conventional_fee
 
 from decimal import Decimal
 import time
 
 BASE_ARGS = [
-    '-minrelaytxfee=0',
     nuparams(NU5_BRANCH_ID, 210),
     '-regtestwalletsetbestchaineveryblock',
 ]
@@ -49,10 +49,11 @@ def run_test(self):
 
         # Create a transaction with an Orchard output to advance the Orchard
         # commitment tree.
-        recipients = [{'address': ua, 'amount': Decimal('10')}]
+        coinbase_fee = conventional_fee(3)
+        recipients = [{'address': ua, 'amount': Decimal('10') - coinbase_fee}]
         myopid = self.nodes[0].z_sendmany(
             get_coinbase_address(self.nodes[0]),
-            recipients, 1, 0, 'AllowRevealedSenders')
+            recipients, 1, coinbase_fee, 'AllowRevealedSenders')
         wait_and_assert_operationid_status(self.nodes[0], myopid)
 
         # Mine the transaction.
@@ -62,7 +63,7 @@ def run_test(self):
 
         # Confirm that we see the Orchard note in the wallet.
         assert_equal(
-            {'pools': {'orchard': {'valueZat': Decimal('10') * COIN}}, 'minimum_confirmations': 1},
+            {'pools': {'orchard': {'valueZat': (Decimal('10') - coinbase_fee) * COIN}}, 'minimum_confirmations': 1},
             self.nodes[0].z_getbalanceforaccount(acct))
 
         # Mine blocks to ensure that the wallet's tip is far enough beyond NU5
@@ -90,7 +91,7 @@ def run_test(self):
 
         # Confirm that we still see the Orchard note in the wallet.
         assert_equal(
-            {'pools': {'orchard': {'valueZat': Decimal('10') * COIN}}, 'minimum_confirmations': 1},
+            {'pools': {'orchard': {'valueZat': (Decimal('10') - coinbase_fee) * COIN}}, 'minimum_confirmations': 1},
             self.nodes[0].z_getbalanceforaccount(acct))
 
 if __name__ == '__main__':
diff --git a/qa/rpc-tests/wallet_overwintertx.py b/qa/rpc-tests/wallet_overwintertx.py
index c379bc265e2..8d174b8ed4d 100755
--- a/qa/rpc-tests/wallet_overwintertx.py
+++ b/qa/rpc-tests/wallet_overwintertx.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright (c) 2018 The Zcash developers
+# Copyright (c) 2018-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
@@ -7,12 +7,13 @@
 from test_framework.util import (
     assert_equal,
     assert_greater_than,
+    assert_raises_message,
     connect_nodes_bi,
     get_coinbase_address,
     start_nodes,
     wait_and_assert_operationid_status,
-    LEGACY_DEFAULT_FEE
 )
+from test_framework.zip317 import conventional_fee
 from test_framework.authproxy import JSONRPCException
 
 from decimal import Decimal
@@ -58,6 +59,7 @@ def run_test (self):
         # Currently at block 195. The next block to be mined 196 is a Sapling block
         #
         bci = self.nodes[0].getblockchaininfo()
+        assert_equal(bci['blocks'], 195)
         assert_equal(bci['consensus']['chaintip'], '76b809bb')
         assert_equal(bci['consensus']['nextblock'], '76b809bb')
         assert_equal(bci['upgrades']['2bb40e60']['status'], 'pending')
@@ -68,14 +70,14 @@ def run_test (self):
         self.sync_all()
 
         # Node 2 sends the zero-confirmation transparent funds to Node 1 using z_sendmany
-        recipients = []
-        recipients.append({"address":taddr1, "amount": Decimal('0.5')})
-        myopid = self.nodes[2].z_sendmany(taddr2, recipients, 0, LEGACY_DEFAULT_FEE, 'AllowFullyTransparent')
+        fee = conventional_fee(2)
+        recipients = [{"address": taddr1, "amount": Decimal('0.5')}]
+        myopid = self.nodes[2].z_sendmany(taddr2, recipients, 0, fee, 'AllowFullyTransparent')
         txid_zsendmany = wait_and_assert_operationid_status(self.nodes[2], myopid)
 
-        # Node 0 shields to Node 2, a coinbase utxo of value 10.0 less default fee
-        zsendamount = Decimal('10.0') - LEGACY_DEFAULT_FEE
-        result = self.nodes[0].z_shieldcoinbase(taddr0, zaddr2, LEGACY_DEFAULT_FEE, 1)
+        # Node 0 shields to Node 2, a coinbase utxo of value 10.0 less fee
+        shield_fee = conventional_fee(3)
+        result = self.nodes[0].z_shieldcoinbase(taddr0, zaddr2, shield_fee, 1)
         txid_shielded = wait_and_assert_operationid_status(self.nodes[0], result['opid'])
 
         # Skip over the three blocks prior to activation; no transactions can be mined
@@ -86,8 +88,8 @@ def run_test (self):
 
         # Verify balance
         assert_equal(self.nodes[1].z_getbalance(taddr1), Decimal('0.5'))
-        assert_equal(self.nodes[2].getbalance(), Decimal('0.5') - LEGACY_DEFAULT_FEE)
-        assert_equal(self.nodes[2].z_getbalance(zaddr2), zsendamount)
+        assert_equal(self.nodes[2].getbalance(), Decimal('0.5') - fee)
+        assert_equal(self.nodes[2].z_getbalance(zaddr2), Decimal('10.0') - shield_fee)
 
         # Verify transaction version is 4 (intended for Sapling+)
         result = self.nodes[0].getrawtransaction(txid_transparent, 1)
@@ -104,32 +106,20 @@ def run_test (self):
         # Currently at block 199. The next block to be mined 200 is a Blossom block
         #
         bci = self.nodes[0].getblockchaininfo()
+        assert_equal(bci['blocks'], 199)
         assert_equal(bci['consensus']['chaintip'], '76b809bb')
         assert_equal(bci['consensus']['nextblock'], '2bb40e60')
         assert_equal(bci['upgrades']['2bb40e60']['status'], 'pending')
 
         # Test using expiryheight parameter of createrawtransaction when Blossom is active in the next block
-        errorString = ""
-        try:
-            self.nodes[0].createrawtransaction([], {}, 0, 499999999)
-        except JSONRPCException as e:
-            errorString = e.error['message']
-        assert_equal("", errorString)
-        try:
-            self.nodes[0].createrawtransaction([], {}, 0, -1)
-        except JSONRPCException as e:
-            errorString = e.error['message']
-        assert_equal("Invalid parameter, expiryheight must be nonnegative and less than 500000000" in errorString, True)
-        try:
-            self.nodes[0].createrawtransaction([], {}, 0, 500000000)
-        except JSONRPCException as e:
-            errorString = e.error['message']
-        assert_equal("Invalid parameter, expiryheight must be nonnegative and less than 500000000" in errorString, True)
-        try:
-            self.nodes[0].createrawtransaction([], {}, 0, 200)
-        except JSONRPCException as e:
-            errorString = e.error['message']
-        assert_equal("Invalid parameter, expiryheight should be at least 203 to avoid transaction expiring soon" in errorString, True)
+        assert_equal(type(self.nodes[0].createrawtransaction([], {}, 0, 499999999)), str)
+
+        assert_raises_message(JSONRPCException, "Invalid parameter, expiryheight must be nonnegative and less than 500000000",
+                              self.nodes[0].createrawtransaction, [], {}, 0, -1)
+        assert_raises_message(JSONRPCException, "Invalid parameter, expiryheight must be nonnegative and less than 500000000",
+                              self.nodes[0].createrawtransaction, [], {}, 0, 500000000)
+        assert_raises_message(JSONRPCException, "Invalid parameter, expiryheight should be at least 203 to avoid transaction expiring soon",
+                              self.nodes[0].createrawtransaction, [], {}, 0, 200)
 
         # Node 0 sends transparent funds to Node 3
         tsendamount = Decimal('1.0')
@@ -137,14 +127,12 @@ def run_test (self):
         self.sync_all()
 
         # Node 3 sends the zero-confirmation transparent funds to Node 1 using z_sendmany
-        recipients = []
-        recipients.append({"address":taddr1, "amount": Decimal('0.5')})
-        myopid = self.nodes[3].z_sendmany(taddr3, recipients, 0, LEGACY_DEFAULT_FEE, 'AllowFullyTransparent')
+        recipients = [{"address": taddr1, "amount": Decimal('0.5')}]
+        myopid = self.nodes[3].z_sendmany(taddr3, recipients, 0, fee, 'AllowFullyTransparent')
         txid_zsendmany = wait_and_assert_operationid_status(self.nodes[3], myopid)
 
-        # Node 0 shields to Node 3, a coinbase utxo of value 10.0 less default fee
-        zsendamount = Decimal('10.0') - LEGACY_DEFAULT_FEE
-        result = self.nodes[0].z_shieldcoinbase(taddr0, zaddr3, LEGACY_DEFAULT_FEE, 1)
+        # Node 0 shields to Node 3, a coinbase utxo of value 10.0 less fee
+        result = self.nodes[0].z_shieldcoinbase(taddr0, zaddr3, shield_fee, 1)
         txid_shielded = wait_and_assert_operationid_status(self.nodes[0], result['opid'])
 
         # Mine the first Blossom block
@@ -156,16 +144,17 @@ def run_test (self):
         # size_on_disk should be > 0
         assert_greater_than(bci['size_on_disk'], 0)
 
+        assert_equal(bci['blocks'], 200)
         assert_equal(bci['consensus']['chaintip'], '2bb40e60')
         assert_equal(bci['consensus']['nextblock'], '2bb40e60')
         assert_equal(bci['upgrades']['2bb40e60']['status'], 'active')
 
         # Verify balance
         assert_equal(self.nodes[1].z_getbalance(taddr1), Decimal('1.0'))
-        assert_equal(self.nodes[3].getbalance(), Decimal('0.5') - LEGACY_DEFAULT_FEE)
-        assert_equal(self.nodes[3].z_getbalance(zaddr3), zsendamount)
+        assert_equal(self.nodes[3].getbalance(), Decimal('0.5') - fee)
+        assert_equal(self.nodes[3].z_getbalance(zaddr3), Decimal('10.0') - shield_fee)
 
-        # Verify transaction version is 4 (intended for Sapling+)
+        # Verify transaction version is 4 (Sapling onward)
         result = self.nodes[0].getrawtransaction(txid_transparent, 1)
         assert_equal(result["version"], 4)
         assert_equal(result["overwintered"], True)
diff --git a/qa/rpc-tests/wallet_persistence.py b/qa/rpc-tests/wallet_persistence.py
index 74085e6b069..a9774880bd1 100755
--- a/qa/rpc-tests/wallet_persistence.py
+++ b/qa/rpc-tests/wallet_persistence.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright (c) 2018 The Zcash developers
+# Copyright (c) 2018-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
@@ -12,6 +12,8 @@
     initialize_chain_clean, connect_nodes_bi, wait_bitcoinds,
     wait_and_assert_operationid_status
 )
+from test_framework.zip317 import conventional_fee
+
 from decimal import Decimal
 
 class WalletPersistenceTest (BitcoinTestFramework):
@@ -22,7 +24,6 @@ def setup_chain(self):
 
     def setup_network(self, split=False):
         self.nodes = start_nodes(4, self.options.tmpdir, extra_args=[[
-            '-minrelaytxfee=0',
             '-allowdeprecated=z_getnewaddress',
             '-allowdeprecated=z_getbalance',
             '-allowdeprecated=z_gettotalbalance',
@@ -95,9 +96,9 @@ def check_chain_value(pool, expected_id, expected_value):
 
         # Node 0 shields funds to Sapling address
         taddr0 = get_coinbase_address(self.nodes[0])
-        recipients = []
-        recipients.append({"address": sapling_addr, "amount": Decimal('20')})
-        myopid = self.nodes[0].z_sendmany(taddr0, recipients, 1, 0, 'AllowRevealedSenders')
+        fee = conventional_fee(4)
+        recipients = [{"address": sapling_addr, "amount": Decimal('20') - fee}]
+        myopid = self.nodes[0].z_sendmany(taddr0, recipients, 1, fee, 'AllowRevealedSenders')
         wait_and_assert_operationid_status(self.nodes[0], myopid)
 
         self.sync_all()
@@ -106,15 +107,15 @@ def check_chain_value(pool, expected_id, expected_value):
         self.sync_all()
 
         # Verify shielded balance
-        assert_equal(self.nodes[0].z_getbalance(sapling_addr), Decimal('20'))
+        assert_equal(Decimal(self.nodes[0].z_getbalance(sapling_addr)), Decimal('20') - fee)
 
         # Verify size of pools
         chainInfo = self.nodes[0].getblockchaininfo()
         pools = chainInfo['valuePools']
         check_chain_value(chainInfo['chainSupply'], None, expected_supply)  # Supply
-        check_chain_value(pools[0], 'transparent', expected_supply - Decimal('20'))  # Transparent
+        check_chain_value(pools[0], 'transparent', expected_supply - (Decimal('20') - fee))  # Transparent
         check_chain_value(pools[1], 'sprout',  Decimal('0'))  
-        check_chain_value(pools[2], 'sapling', Decimal('20'))
+        check_chain_value(pools[2], 'sapling', Decimal('20') - fee)
         check_chain_value(pools[3], 'orchard', Decimal('0'))
 
         # Restart the nodes
@@ -126,16 +127,15 @@ def check_chain_value(pool, expected_id, expected_value):
         chainInfo = self.nodes[0].getblockchaininfo()
         pools = chainInfo['valuePools']
         check_chain_value(chainInfo['chainSupply'], None, expected_supply)  # Supply
-        check_chain_value(pools[0], 'transparent', expected_supply - Decimal('20'))  # Transparent
+        check_chain_value(pools[0], 'transparent', expected_supply - (Decimal('20') - fee))  # Transparent
         check_chain_value(pools[1], 'sprout',  Decimal('0')) 
-        check_chain_value(pools[2], 'sapling', Decimal('20'))
+        check_chain_value(pools[2], 'sapling', Decimal('20') - fee)
         check_chain_value(pools[3], 'orchard', Decimal('0'))
 
         # Node 0 sends some shielded funds to Node 1
         dest_addr = self.nodes[1].z_getnewaddress('sapling')
-        recipients = []
-        recipients.append({"address": dest_addr, "amount": Decimal('15')})
-        myopid = self.nodes[0].z_sendmany(sapling_addr, recipients, 1, 0)
+        recipients = [{"address": dest_addr, "amount": Decimal('15') - fee}]
+        myopid = self.nodes[0].z_sendmany(sapling_addr, recipients, 1, fee)
         wait_and_assert_operationid_status(self.nodes[0], myopid)
 
         self.sync_all()
@@ -143,8 +143,8 @@ def check_chain_value(pool, expected_id, expected_value):
         self.sync_all()
 
         # Verify balances
-        assert_equal(self.nodes[0].z_getbalance(sapling_addr), Decimal('5'))
-        assert_equal(self.nodes[1].z_getbalance(dest_addr), Decimal('15'))
+        assert_equal(Decimal(self.nodes[0].z_getbalance(sapling_addr)), Decimal('5') - fee)
+        assert_equal(Decimal(self.nodes[1].z_getbalance(dest_addr)), Decimal('15') - fee)
 
         # Restart the nodes
         stop_nodes(self.nodes)
@@ -152,20 +152,20 @@ def check_chain_value(pool, expected_id, expected_value):
         self.setup_network()
 
         # Verify balances
-        assert_equal(self.nodes[0].z_getbalance(sapling_addr), Decimal('5'))
-        assert_equal(self.nodes[1].z_getbalance(dest_addr), Decimal('15'))
+        assert_equal(Decimal(self.nodes[0].z_getbalance(sapling_addr)), Decimal('5') - fee)
+        assert_equal(Decimal(self.nodes[1].z_getbalance(dest_addr)), Decimal('15') - fee)
 
         # Verify importing a spending key will update and persist the nullifiers and witnesses correctly
         sk0 = self.nodes[0].z_exportkey(sapling_addr)
         self.nodes[2].z_importkey(sk0, "yes")
-        assert_equal(self.nodes[2].z_getbalance(sapling_addr), Decimal('5'))
+        assert_equal(Decimal(self.nodes[2].z_getbalance(sapling_addr)), Decimal('5') - fee)
 
         # Verify importing a viewing key will update and persist the nullifiers and witnesses correctly
         extfvk0 = self.nodes[0].z_exportviewingkey(sapling_addr)
         self.nodes[3].z_importviewingkey(extfvk0, "yes")
-        assert_equal(self.nodes[3].z_getbalance(sapling_addr), Decimal('5'))
-        assert_equal(self.nodes[3].z_gettotalbalance()['private'], '0.00')
-        assert_equal(self.nodes[3].z_gettotalbalance(1, True)['private'], '5.00')
+        assert_equal(Decimal(self.nodes[3].z_getbalance(sapling_addr)), Decimal('5') - fee)
+        assert_equal(Decimal(self.nodes[3].z_gettotalbalance()['private']), Decimal('0'))
+        assert_equal(Decimal(self.nodes[3].z_gettotalbalance(1, True)['private']), Decimal('5') - fee)
 
         # Restart the nodes
         stop_nodes(self.nodes)
@@ -175,15 +175,14 @@ def check_chain_value(pool, expected_id, expected_value):
         # Verify nullifiers persisted correctly by checking balance
         # Prior to PR #3590, there will be an error as spent notes are considered unspent:
         #    Assertion failed: expected: <25.00000000> but was: <5>
-        assert_equal(self.nodes[2].z_getbalance(sapling_addr), Decimal('5'))
-        assert_equal(self.nodes[3].z_getbalance(sapling_addr), Decimal('5'))
-        assert_equal(self.nodes[3].z_gettotalbalance()['private'], '0.00')
-        assert_equal(self.nodes[3].z_gettotalbalance(1, True)['private'], '5.00')
+        assert_equal(Decimal(self.nodes[2].z_getbalance(sapling_addr)), Decimal('5') - fee)
+        assert_equal(Decimal(self.nodes[3].z_getbalance(sapling_addr)), Decimal('5') - fee)
+        assert_equal(Decimal(self.nodes[3].z_gettotalbalance()['private']), Decimal('0'))
+        assert_equal(Decimal(self.nodes[3].z_gettotalbalance(1, True)['private']), Decimal('5') - fee)
 
         # Verity witnesses persisted correctly by sending shielded funds
-        recipients = []
-        recipients.append({"address": dest_addr, "amount": Decimal('1')})
-        myopid = self.nodes[2].z_sendmany(sapling_addr, recipients, 1, 0)
+        recipients = [{"address": dest_addr, "amount": Decimal('1')}]
+        myopid = self.nodes[2].z_sendmany(sapling_addr, recipients, 1, fee)
         wait_and_assert_operationid_status(self.nodes[2], myopid)
 
         self.sync_all()
@@ -191,8 +190,8 @@ def check_chain_value(pool, expected_id, expected_value):
         self.sync_all()
 
         # Verify balances
-        assert_equal(self.nodes[2].z_getbalance(sapling_addr), Decimal('4'))
-        assert_equal(self.nodes[1].z_getbalance(dest_addr), Decimal('16'))
+        assert_equal(Decimal(self.nodes[2].z_getbalance(sapling_addr)), Decimal('4') - 2*fee)
+        assert_equal(Decimal(self.nodes[1].z_getbalance(dest_addr)), Decimal('16') - fee)
 
 if __name__ == '__main__':
     WalletPersistenceTest().main()
diff --git a/qa/rpc-tests/wallet_sapling.py b/qa/rpc-tests/wallet_sapling.py
index 609a35437bd..9bf15502b14 100755
--- a/qa/rpc-tests/wallet_sapling.py
+++ b/qa/rpc-tests/wallet_sapling.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright (c) 2018 The Zcash developers
+# Copyright (c) 2018-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
@@ -9,8 +9,8 @@
     get_coinbase_address,
     start_nodes,
     wait_and_assert_operationid_status,
-    LEGACY_DEFAULT_FEE
 )
+from test_framework.zip317 import ZIP_317_FEE, conventional_fee
 
 from decimal import Decimal
 
@@ -19,7 +19,6 @@ class WalletSaplingTest(BitcoinTestFramework):
 
     def setup_nodes(self):
         return start_nodes(self.num_nodes, self.options.tmpdir, extra_args=[[
-            '-minrelaytxfee=0',
             '-allowdeprecated=getnewaddress',
             '-allowdeprecated=z_getnewaddress',
             '-allowdeprecated=z_getbalance',
@@ -42,21 +41,21 @@ def run_test(self):
         assert_equal(self.nodes[0].z_validateaddress(saplingAddr1)['address_type'], 'sapling')
 
         # Verify balance
-        assert_equal(self.nodes[0].z_getbalance(saplingAddr0), Decimal('0'))
-        assert_equal(self.nodes[1].z_getbalance(saplingAddr1), Decimal('0'))
-        assert_equal(self.nodes[1].z_getbalance(taddr1), Decimal('0'))
+        assert_equal(Decimal(self.nodes[0].z_getbalance(saplingAddr0)), Decimal('0'))
+        assert_equal(Decimal(self.nodes[1].z_getbalance(saplingAddr1)), Decimal('0'))
+        assert_equal(Decimal(self.nodes[1].z_getbalance(taddr1)), Decimal('0'))
 
         # Node 0 shields some funds
         # taddr -> Sapling
-        recipients = []
-        recipients.append({"address": saplingAddr0, "amount": Decimal('10')})
-        myopid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, 0, 'AllowRevealedSenders')
+        coinbase_fee = conventional_fee(3)
+        recipients = [{"address": saplingAddr0, "amount": Decimal('10') - coinbase_fee}]
+        myopid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, coinbase_fee, 'AllowRevealedSenders')
         mytxid = wait_and_assert_operationid_status(self.nodes[0], myopid)
 
         self.sync_all()
 
         # Shield another coinbase UTXO
-        myopid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, 0, 'AllowRevealedSenders')
+        myopid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, coinbase_fee, 'AllowRevealedSenders')
         mytxid = wait_and_assert_operationid_status(self.nodes[0], myopid)
 
         self.sync_all()
@@ -64,16 +63,17 @@ def run_test(self):
         self.sync_all()
 
         # Verify balance
-        assert_equal(self.nodes[0].z_getbalance(saplingAddr0), Decimal('20'))
-        assert_equal(self.nodes[1].z_getbalance(saplingAddr1), Decimal('0'))
-        assert_equal(self.nodes[1].z_getbalance(taddr1), Decimal('0'))
+        balance0 = Decimal('20') - 2 * coinbase_fee
+        assert_equal(Decimal(self.nodes[0].z_getbalance(saplingAddr0)), balance0)
+        assert_equal(Decimal(self.nodes[1].z_getbalance(saplingAddr1)), Decimal('0'))
+        assert_equal(Decimal(self.nodes[1].z_getbalance(taddr1)), Decimal('0'))
 
         # Node 0 sends some shielded funds to node 1
         # Sapling -> Sapling
         #         -> Sapling (change)
         recipients = []
         recipients.append({"address": saplingAddr1, "amount": Decimal('15')})
-        myopid = self.nodes[0].z_sendmany(saplingAddr0, recipients, 1, 0)
+        myopid = self.nodes[0].z_sendmany(saplingAddr0, recipients, 1, ZIP_317_FEE)
         mytxid = wait_and_assert_operationid_status(self.nodes[0], myopid)
 
         self.sync_all()
@@ -81,9 +81,11 @@ def run_test(self):
         self.sync_all()
 
         # Verify balance
-        assert_equal(self.nodes[0].z_getbalance(saplingAddr0), Decimal('5'))
-        assert_equal(self.nodes[1].z_getbalance(saplingAddr1), Decimal('15'))
-        assert_equal(self.nodes[1].z_getbalance(taddr1), Decimal('0'))
+        balance1 = Decimal('15')
+        balance0 -= balance1 + conventional_fee(2)
+        assert_equal(Decimal(self.nodes[0].z_getbalance(saplingAddr0)), balance0)
+        assert_equal(Decimal(self.nodes[1].z_getbalance(saplingAddr1)), balance1)
+        assert_equal(Decimal(self.nodes[1].z_getbalance(taddr1)), Decimal('0'))
 
         # Node 1 sends some shielded funds to node 0, as well as unshielding
         # Sapling -> Sapling
@@ -92,7 +94,7 @@ def run_test(self):
         recipients = []
         recipients.append({"address": saplingAddr0, "amount": Decimal('5')})
         recipients.append({"address": taddr1, "amount": Decimal('5')})
-        myopid = self.nodes[1].z_sendmany(saplingAddr1, recipients, 1, 0, 'AllowRevealedRecipients')
+        myopid = self.nodes[1].z_sendmany(saplingAddr1, recipients, 1, ZIP_317_FEE, 'AllowRevealedRecipients')
         mytxid = wait_and_assert_operationid_status(self.nodes[1], myopid)
 
         self.sync_all()
@@ -100,13 +102,16 @@ def run_test(self):
         self.sync_all()
 
         # Verify balance
-        assert_equal(self.nodes[0].z_getbalance(saplingAddr0), Decimal('10'))
-        assert_equal(self.nodes[1].z_getbalance(saplingAddr1), Decimal('5'))
-        assert_equal(self.nodes[1].z_getbalance(taddr1), Decimal('5'))
-
-        # Verify existence of Sapling related JSON fields
+        fee = conventional_fee(3)
+        balance0 += Decimal('5')
+        balance1 -= Decimal('10') + fee
+        assert_equal(Decimal(self.nodes[0].z_getbalance(saplingAddr0)), balance0)
+        assert_equal(Decimal(self.nodes[1].z_getbalance(saplingAddr1)), balance1)
+        assert_equal(Decimal(self.nodes[1].z_getbalance(taddr1)), Decimal('5'))
+
+        # Verify existence of Sapling-related JSON fields
         resp = self.nodes[0].getrawtransaction(mytxid, 1)
-        assert_equal(resp['valueBalance'], Decimal('5'))
+        assert_equal(resp['valueBalance'], Decimal('5') + fee)
         assert(len(resp['vShieldedSpend']) == 1)
         assert(len(resp['vShieldedOutput']) == 2)
         assert('bindingSig' in resp)
@@ -130,37 +135,38 @@ def run_test(self):
         saplingAddrInfo0 = self.nodes[2].z_importkey(sk0, "yes")
         assert_equal(saplingAddrInfo0["address_type"], "sapling")
         assert_equal(saplingAddrInfo0["address"], saplingAddr0)
-        assert_equal(self.nodes[2].z_getbalance(saplingAddrInfo0["address"]), Decimal('10'))
+        assert_equal(Decimal(self.nodes[2].z_getbalance(saplingAddrInfo0["address"])), balance0)
         sk1 = self.nodes[1].z_exportkey(saplingAddr1)
         saplingAddrInfo1 = self.nodes[2].z_importkey(sk1, "yes")
         assert_equal(saplingAddrInfo1["address_type"], "sapling")
         assert_equal(saplingAddrInfo1["address"], saplingAddr1)
-        assert_equal(self.nodes[2].z_getbalance(saplingAddrInfo1["address"]), Decimal('5'))
+        assert_equal(Decimal(self.nodes[2].z_getbalance(saplingAddrInfo1["address"])), balance1)
 
         # Verify importing a viewing key will update the nullifiers and witnesses correctly
         extfvk0 = self.nodes[0].z_exportviewingkey(saplingAddr0)
         saplingAddrInfo0 = self.nodes[3].z_importviewingkey(extfvk0, "yes")
         assert_equal(saplingAddrInfo0["address_type"], "sapling")
         assert_equal(saplingAddrInfo0["address"], saplingAddr0)
-        assert_equal(self.nodes[3].z_getbalance(saplingAddrInfo0["address"]), Decimal('10'))
+        assert_equal(Decimal(self.nodes[3].z_getbalance(saplingAddrInfo0["address"])), balance0)
         extfvk1 = self.nodes[1].z_exportviewingkey(saplingAddr1)
         saplingAddrInfo1 = self.nodes[3].z_importviewingkey(extfvk1, "yes")
         assert_equal(saplingAddrInfo1["address_type"], "sapling")
         assert_equal(saplingAddrInfo1["address"], saplingAddr1)
-        assert_equal(self.nodes[3].z_getbalance(saplingAddrInfo1["address"]), Decimal('5'))
+        assert_equal(Decimal(self.nodes[3].z_getbalance(saplingAddrInfo1["address"])), balance1)
 
         # Verify that z_gettotalbalance only includes watch-only addresses when requested
-        assert_equal(self.nodes[3].z_gettotalbalance()['private'], '0.00')
-        assert_equal(self.nodes[3].z_gettotalbalance(1, True)['private'], '15.00')
+        assert_equal(Decimal(self.nodes[3].z_gettotalbalance()['private']), Decimal('0.00'))
+        assert_equal(Decimal(self.nodes[3].z_gettotalbalance(1, True)['private']), balance0 + balance1)
 
         # Make sure we get a useful error when trying to send to both sprout and sapling
         node4_sproutaddr = self.nodes[3].z_getnewaddress('sprout')
         node4_saplingaddr = self.nodes[3].z_getnewaddress('sapling')
+        fee = conventional_fee(5)
         myopid = self.nodes[1].z_sendmany(
             taddr1,
             [{'address': node4_sproutaddr, 'amount': Decimal('2.5')},
-             {'address': node4_saplingaddr, 'amount': Decimal('2.5') - LEGACY_DEFAULT_FEE}],
-            1, LEGACY_DEFAULT_FEE, 'AllowRevealedSenders'
+             {'address': node4_saplingaddr, 'amount': Decimal('2.5') - fee}],
+            1, fee, 'AllowRevealedSenders'
         )
         wait_and_assert_operationid_status(self.nodes[1], myopid, "failed", "Sending funds into the Sprout pool is no longer supported.")
 
diff --git a/qa/rpc-tests/wallet_sendmany_any_taddr.py b/qa/rpc-tests/wallet_sendmany_any_taddr.py
index 61e2ee26104..5ff8b7381a6 100755
--- a/qa/rpc-tests/wallet_sendmany_any_taddr.py
+++ b/qa/rpc-tests/wallet_sendmany_any_taddr.py
@@ -1,11 +1,12 @@
 #!/usr/bin/env python3
-# Copyright (c) 2020 The Zcash developers
+# Copyright (c) 2020-2024 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.test_framework import BitcoinTestFramework
 from test_framework.util import (
-    LEGACY_DEFAULT_FEE,
     assert_equal,
     assert_raises_message,
     connect_nodes_bi,
@@ -13,7 +14,7 @@
     sync_blocks,
     wait_and_assert_operationid_status,
 )
-from test_framework.zip317 import ZIP_317_FEE
+from test_framework.zip317 import conventional_fee, ZIP_317_FEE
 
 TX_EXPIRY_DELTA = 10
 TX_EXPIRING_SOON_THRESHOLD = 3
@@ -22,7 +23,6 @@
 class WalletSendManyAnyTaddr(BitcoinTestFramework):
     def setup_nodes(self):
         return start_nodes(self.num_nodes, self.options.tmpdir, extra_args=[[
-            '-minrelaytxfee=0',
             '-txexpirydelta=%d' % TX_EXPIRY_DELTA,
             '-allowdeprecated=getnewaddress',
             '-allowdeprecated=z_getnewaddress',
@@ -43,44 +43,42 @@ def run_test(self):
         node3taddr2 = self.nodes[3].getnewaddress()
 
         # Prepare some non-coinbase UTXOs
+        fee = conventional_fee(27)
         wait_and_assert_operationid_status(
             self.nodes[3],
-            self.nodes[3].z_shieldcoinbase("*", node3zaddr, 0, None, None, 'AllowLinkingAccountAddresses')['opid'],
+            self.nodes[3].z_shieldcoinbase("*", node3zaddr, fee, None, None, 'AllowLinkingAccountAddresses')['opid'],
         )
         self.sync_all()
         self.nodes[0].generate(1)
         self.sync_all()
-        assert_equal(self.nodes[3].z_getbalance(node3zaddr), 250)
+        assert_equal(Decimal(self.nodes[3].z_getbalance(node3zaddr)), Decimal('250') - fee)
 
         wait_and_assert_operationid_status(
             self.nodes[3],
             self.nodes[3].z_sendmany(
                 node3zaddr,
                 [
-                    {'address': node3taddr1, 'amount': 60},
-                    {'address': node3taddr2, 'amount': 75},
+                    {'address': node3taddr1, 'amount': Decimal('60')},
+                    {'address': node3taddr2, 'amount': Decimal('75')},
                 ],
-                1,
-                LEGACY_DEFAULT_FEE,
-                'AllowRevealedRecipients',
-            ),
+                1, ZIP_317_FEE, 'AllowRevealedRecipients'),
         )
         self.sync_all()
         self.nodes[0].generate(1)
         self.sync_all()
 
         # Check the various balances.
-        assert_equal(self.nodes[1].z_getbalance(recipient), 0)
-        assert_equal(self.nodes[3].z_getbalance(node3taddr1), 60)
-        assert_equal(self.nodes[3].z_getbalance(node3taddr2), 75)
+        assert_equal(Decimal(self.nodes[1].z_getbalance(recipient)), Decimal('0'))
+        assert_equal(Decimal(self.nodes[3].z_getbalance(node3taddr1)), Decimal('60'))
+        assert_equal(Decimal(self.nodes[3].z_getbalance(node3taddr2)), Decimal('75'))
 
         # We should be able to spend multiple UTXOs at once
         wait_and_assert_operationid_status(
             self.nodes[3],
             self.nodes[3].z_sendmany(
                 'ANY_TADDR',
-                [{'address': recipient, 'amount': 100}],
-                1, LEGACY_DEFAULT_FEE, 'NoPrivacy'),
+                [{'address': recipient, 'amount': Decimal('100')}],
+                1, ZIP_317_FEE, 'NoPrivacy'),
         )
 
         self.sync_all()
@@ -88,19 +86,20 @@ def run_test(self):
         self.sync_all()
 
         # The recipient has their funds!
-        assert_equal(self.nodes[1].z_getbalance(recipient), 100)
+        assert_equal(Decimal(self.nodes[1].z_getbalance(recipient)), Decimal('100'))
 
         # Change is sent to a new t-address.
-        assert_equal(self.nodes[3].z_getbalance(node3taddr1), 0)
-        assert_equal(self.nodes[3].z_getbalance(node3taddr2), 0)
+        assert_equal(Decimal(self.nodes[3].z_getbalance(node3taddr1)), Decimal('0'))
+        assert_equal(Decimal(self.nodes[3].z_getbalance(node3taddr2)), Decimal('0'))
 
         # Send from a change t-address.
+        fee = conventional_fee(3)
         wait_and_assert_operationid_status(
             self.nodes[3],
             self.nodes[3].z_sendmany(
                 'ANY_TADDR',
-                [{'address': recipient, 'amount': 20}],
-                1, LEGACY_DEFAULT_FEE, 'AllowFullyTransparent'),
+                [{'address': recipient, 'amount': Decimal('20')}],
+                1, fee, 'AllowFullyTransparent'),
         )
 
         self.sync_all()
@@ -108,18 +107,23 @@ def run_test(self):
         self.sync_all()
 
         # The recipient has their funds!
-        assert_equal(self.nodes[1].z_getbalance(recipient), 120)
+        assert_equal(Decimal(self.nodes[1].z_getbalance(recipient)), Decimal('120'))
 
         # Check that ANY_TADDR note selection doesn't attempt a double-spend
         myopid = self.nodes[3].z_sendmany(
             'ANY_TADDR',
-            [{'address': recipient, 'amount': 20}],
-            1, LEGACY_DEFAULT_FEE, 'AllowLinkingAccountAddresses')
-        wait_and_assert_operationid_status(self.nodes[3], myopid, "failed", "Insufficient funds: have 14.99998, need 20.00001; note that coinbase outputs will not be selected if you specify ANY_TADDR, any transparent recipients are included, or if the `privacyPolicy` parameter is not set to `AllowRevealedSenders` or weaker.")
+            [{'address': recipient, 'amount': Decimal('20') - fee}],
+            1, fee, 'AllowLinkingAccountAddresses')
+        wait_and_assert_operationid_status(self.nodes[3], myopid, "failed", "Insufficient funds: have 14.99965, need 20.00; note that coinbase outputs will not be selected if you specify ANY_TADDR, any transparent recipients are included, or if the `privacyPolicy` parameter is not set to `AllowRevealedSenders` or weaker.")
 
-        # Create an expired transaction on node 3.
+        # Create a transaction that will expire on node 3.
         self.split_network()
-        expire_transparent = self.nodes[3].sendtoaddress(node2taddr1, 14)
+        myopid = self.nodes[3].z_sendmany(
+            'ANY_TADDR',
+            [{'address': node2taddr1, 'amount': Decimal('14')}],
+            1, ZIP_317_FEE, 'AllowFullyTransparent')
+
+        expire_transparent = wait_and_assert_operationid_status(self.nodes[3], myopid)
         assert(expire_transparent in self.nodes[3].getrawmempool())
         self.sync_all()
         assert_equal('waiting', self.nodes[2].gettransaction(expire_transparent)['status'])
@@ -133,26 +137,27 @@ def run_test(self):
         # Ensure that node 2 has no transparent funds.
         self.nodes[2].generate(100) # To ensure node 2's pending coinbase is spendable
         self.sync_all()
-        assert_raises_message(AssertionError, "tx unpaid action limit exceeded",
+        assert_raises_message(AssertionError, "tx unpaid action limit exceeded: 50 action(s) exceeds limit of 0",
             wait_and_assert_operationid_status,
             self.nodes[2],
-            self.nodes[2].z_shieldcoinbase("*", node2zaddr, 0, None, None, 'AllowLinkingAccountAddresses')['opid'],
+            self.nodes[2].z_shieldcoinbase("*", node2zaddr, conventional_fee(2), None, None, 'AllowLinkingAccountAddresses')['opid'],
         )
         wait_and_assert_operationid_status(
             self.nodes[2],
             self.nodes[2].z_shieldcoinbase("*", node2zaddr, ZIP_317_FEE, None, None, 'AllowLinkingAccountAddresses')['opid'],
         )
         self.sync_all()
-        assert_equal(0, self.nodes[2].getbalance())
+        assert_equal(Decimal('0'), Decimal(self.nodes[2].getbalance()))
 
         # Check that ANY_TADDR doesn't select an expired output.
+        fee = conventional_fee(3)
         wait_and_assert_operationid_status(
             self.nodes[2],
             self.nodes[2].z_sendmany(
                 'ANY_TADDR',
-                [{'address': recipient, 'amount': 13}],
-                1, LEGACY_DEFAULT_FEE, 'AllowRevealedSenders'),
-            "failed", "Insufficient funds: have 0.00, need 13.00001; note that coinbase outputs will not be selected if you specify ANY_TADDR, any transparent recipients are included, or if the `privacyPolicy` parameter is not set to `AllowRevealedSenders` or weaker. (This transaction may require selecting transparent coins that were sent to multiple addresses, which is not enabled by default because it would create a public link between those addresses. THIS MAY AFFECT YOUR PRIVACY. Resubmit with the `privacyPolicy` parameter set to `AllowLinkingAccountAddresses` or weaker if you wish to allow this transaction to proceed anyway.)")
+                [{'address': recipient, 'amount': Decimal('13') - fee}],
+                1, fee, 'AllowRevealedSenders'),
+            "failed", "Insufficient funds: have 0.00, need 13.00; note that coinbase outputs will not be selected if you specify ANY_TADDR, any transparent recipients are included, or if the `privacyPolicy` parameter is not set to `AllowRevealedSenders` or weaker. (This transaction may require selecting transparent coins that were sent to multiple addresses, which is not enabled by default because it would create a public link between those addresses. THIS MAY AFFECT YOUR PRIVACY. Resubmit with the `privacyPolicy` parameter set to `AllowLinkingAccountAddresses` or weaker if you wish to allow this transaction to proceed anyway.)")
 
 if __name__ == '__main__':
     WalletSendManyAnyTaddr().main()
diff --git a/qa/rpc-tests/wallet_shieldcoinbase.py b/qa/rpc-tests/wallet_shieldcoinbase.py
index 1718a5cf0d2..e58fc623e91 100755
--- a/qa/rpc-tests/wallet_shieldcoinbase.py
+++ b/qa/rpc-tests/wallet_shieldcoinbase.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright (c) 2017 The Zcash developers
+# Copyright (c) 2017-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
@@ -20,7 +20,6 @@ def setup_chain(self):
 
     def setup_network(self, split=False):
         args = [
-            '-minrelaytxfee=0',
             '-regtestprotectcoinbase',
             '-debug=zrpcunsafe',
             nuparams(NU5_BRANCH_ID, self.nu5_activation),
@@ -45,8 +44,8 @@ def run_test (self):
         self.nodes[0].generate(4)
         self.sync_all()
         walletinfo = self.nodes[0].getwalletinfo()
-        assert_equal(walletinfo['immature_balance'], 50)
-        assert_equal(walletinfo['balance'], 0)
+        assert_equal(Decimal(walletinfo['immature_balance']), Decimal('50'))
+        assert_equal(Decimal(walletinfo['balance']), Decimal('0'))
         self.sync_all()
         self.nodes[2].generate(1)
         self.nodes[2].generate(1)
@@ -54,9 +53,9 @@ def run_test (self):
         self.sync_all()
         self.nodes[1].generate(101)
         self.sync_all()
-        assert_equal(self.nodes[0].getbalance(), 50)
-        assert_equal(self.nodes[1].getbalance(), 10)
-        assert_equal(self.nodes[2].getbalance(), 30)
+        assert_equal(Decimal(self.nodes[0].getbalance()), Decimal('50'))
+        assert_equal(Decimal(self.nodes[1].getbalance()), Decimal('10'))
+        assert_equal(Decimal(self.nodes[2].getbalance()), Decimal('30'))
 
         # create one zaddr that is the target of all shielding
         myzaddr = self.test_init_zaddr(self.nodes[0])
@@ -102,31 +101,33 @@ def run_test (self):
             errorString = e.error['message']
         assert_equal("JSON integer out of range" in errorString, True)
 
-        # Shield coinbase utxos from node 0 of value 40, standard fee
-        result = self.nodes[0].z_shieldcoinbase(mytaddr, myzaddr)
+        # Shield coinbase utxos from node 0 of value 40
+        fee = conventional_fee(6)
+        result = self.nodes[0].z_shieldcoinbase(mytaddr, myzaddr, fee)
         wait_and_assert_operationid_status(self.nodes[0], result['opid'])
         self.sync_all()
         self.nodes[1].generate(1)
         self.sync_all()
 
         # Confirm balances and that do_not_shield_taddr containing funds of 10 was left alone
-        assert_equal(self.nodes[0].getbalance(), 10)
-        assert_equal(self.nodes[0].z_getbalance(do_not_shield_taddr), Decimal('10.0'))
-        self.test_check_balance_zaddr(self.nodes[0], Decimal('40.0') - conventional_fee(6))
-        assert_equal(self.nodes[1].getbalance(), 20)
-        assert_equal(self.nodes[2].getbalance(), 30)
-
-        # Shield coinbase utxos from any node 2 taddr, and set fee to 0
-        result = self.nodes[2].z_shieldcoinbase("*", myzaddr, 0, None, None, 'AllowLinkingAccountAddresses')
+        assert_equal(Decimal(self.nodes[0].getbalance()), Decimal('10'))
+        assert_equal(Decimal(self.nodes[0].z_getbalance(do_not_shield_taddr)), Decimal('10'))
+        self.test_check_balance_zaddr(self.nodes[0], Decimal('40') - fee)
+        assert_equal(Decimal(self.nodes[1].getbalance()), Decimal('20'))
+        assert_equal(Decimal(self.nodes[2].getbalance()), Decimal('30'))
+
+        # Shield coinbase utxos from any node 2 taddr
+        fee2 = conventional_fee(5)
+        result = self.nodes[2].z_shieldcoinbase("*", myzaddr, fee2, None, None, 'AllowLinkingAccountAddresses')
         wait_and_assert_operationid_status(self.nodes[2], result['opid'])
         self.sync_all()
         self.nodes[1].generate(1)
         self.sync_all()
 
-        assert_equal(self.nodes[0].getbalance(), 10)
-        self.test_check_balance_zaddr(self.nodes[0], Decimal('70.0') - conventional_fee(6))
-        assert_equal(self.nodes[1].getbalance(), 30)
-        assert_equal(self.nodes[2].getbalance(), 0)
+        assert_equal(Decimal(self.nodes[0].getbalance()), Decimal('10'))
+        self.test_check_balance_zaddr(self.nodes[0], Decimal('70') - fee - fee2)
+        assert_equal(Decimal(self.nodes[1].getbalance()), Decimal('30'))
+        assert_equal(Decimal(self.nodes[2].getbalance()), Decimal('0'))
 
         # Generate 800 coinbase utxos on node 0, and 20 coinbase utxos on node 2
         self.nodes[0].generate(800)
@@ -139,17 +140,17 @@ def run_test (self):
 
         def verify_locking(first, second, limit):
             result = self.nodes[0].z_shieldcoinbase(mytaddr, myzaddr, ZIP_317_FEE, limit)
-            assert_equal(result["shieldingUTXOs"], Decimal(first))
-            assert_equal(result["remainingUTXOs"], Decimal(second))
+            assert_equal(Decimal(result["shieldingUTXOs"]), Decimal(first))
+            assert_equal(Decimal(result["remainingUTXOs"]), Decimal(second))
             remainingValue = result["remainingValue"]
             opid1 = result['opid']
 
             # Verify that utxos are locked (not available for selection) by queuing up another shielding operation
             result = self.nodes[0].z_shieldcoinbase(mytaddr, myzaddr, ZIP_317_FEE, 0)
-            assert_equal(result["shieldingValue"], Decimal(remainingValue))
-            assert_equal(result["shieldingUTXOs"], Decimal(second))
-            assert_equal(result["remainingValue"], Decimal('0'))
-            assert_equal(result["remainingUTXOs"], Decimal('0'))
+            assert_equal(Decimal(result["shieldingValue"]), Decimal(remainingValue))
+            assert_equal(Decimal(result["shieldingUTXOs"]), Decimal(second))
+            assert_equal(Decimal(result["remainingValue"]), Decimal('0'))
+            assert_equal(Decimal(result["remainingUTXOs"]), Decimal('0'))
             opid2 = result['opid']
 
             # wait for both async operations to complete
@@ -172,14 +173,14 @@ def verify_locking(first, second, limit):
         self.sync_all()
         mytaddr = get_coinbase_address(self.nodes[0], 100)
         result = self.nodes[0].z_shieldcoinbase(mytaddr, myzaddr, ZIP_317_FEE, None, 'DEADBEEF')
-        assert_equal(result["shieldingUTXOs"], Decimal('50'))
-        assert_equal(result["remainingUTXOs"], Decimal('50'))
+        assert_equal(Decimal(result["shieldingUTXOs"]), Decimal('50'))
+        assert_equal(Decimal(result["remainingUTXOs"]), Decimal('50'))
         wait_and_assert_operationid_status(self.nodes[0], result['opid'])
 
         # Verify maximum number of utxos which node 0 can shield can be set by the limit parameter
         result = self.nodes[0].z_shieldcoinbase(mytaddr, myzaddr, ZIP_317_FEE, 33, None)
-        assert_equal(result["shieldingUTXOs"], Decimal('33'))
-        assert_equal(result["remainingUTXOs"], Decimal('17'))
+        assert_equal(Decimal(result["shieldingUTXOs"]), Decimal('33'))
+        assert_equal(Decimal(result["remainingUTXOs"]), Decimal('17'))
         wait_and_assert_operationid_status(self.nodes[0], result['opid'])
         # Don't sync node 2 which rejects the tx due to its mempooltxinputlimit
         sync_blocks(self.nodes[:2])
diff --git a/qa/rpc-tests/wallet_shieldingcoinbase.py b/qa/rpc-tests/wallet_shieldingcoinbase.py
index 49733966904..a8ad203b63e 100755
--- a/qa/rpc-tests/wallet_shieldingcoinbase.py
+++ b/qa/rpc-tests/wallet_shieldingcoinbase.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright (c) 2016 The Zcash developers
+# Copyright (c) 2016-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
@@ -9,7 +9,7 @@
 from test_framework.util import assert_equal, initialize_chain_clean, \
     start_nodes, connect_nodes_bi, wait_and_assert_operationid_status, \
     wait_and_assert_operationid_status_result, get_coinbase_address, \
-    check_node_log, LEGACY_DEFAULT_FEE
+    check_node_log
 from test_framework.zip317 import conventional_fee, WEIGHT_RATIO_CAP, ZIP_317_FEE
 
 import sys
@@ -36,7 +36,6 @@ def setup_chain(self):
     # Start nodes with -regtestshieldcoinbase to set fCoinbaseMustBeShielded to true.
     def setup_network(self, split=False):
         self.nodes = start_nodes(4, self.options.tmpdir, extra_args=[[
-            '-minrelaytxfee=0',
             '-regtestshieldcoinbase',
             '-debug=zrpcunsafe',
             '-allowdeprecated=getnewaddress',
@@ -91,7 +90,7 @@ def run_test (self):
 
         # Node 3 will test that watch only address utxos are not selected
         self.nodes[3].importaddress(mytaddr)
-        recipients= [{"address":myzaddr, "amount": Decimal('1')}]
+        recipients= [{"address": myzaddr, "amount": Decimal('1')}]
         try:
             myopid = self.nodes[3].z_sendmany(mytaddr, recipients)
         except JSONRPCException as e:
@@ -103,20 +102,20 @@ def run_test (self):
         # TODO: After upgrading to unified address support, change will be sent to the most
         # recent shielded spend authority corresponding to the account of the source address
         # and this send will succeed, causing this test to fail.
-        recipients = []
-        recipients.append({"address":myzaddr, "amount":Decimal('1.23456789')})
+        fee = conventional_fee(2)
+        recipients = [{"address": myzaddr, "amount": Decimal('1.23456789')}]
 
-        myopid = self.nodes[0].z_sendmany(mytaddr, recipients, 10, LEGACY_DEFAULT_FEE, 'AllowFullyTransparent')
+        myopid = self.nodes[0].z_sendmany(mytaddr, recipients, 10, fee, 'AllowFullyTransparent')
         error_result = wait_and_assert_operationid_status_result(
                 self.nodes[0],
                 myopid, "failed",
-                "When shielding coinbase funds, the wallet does not allow any change. The proposed transaction would result in 8.76542211 in change.",
+                "When shielding coinbase funds, the wallet does not allow any change. The proposed transaction would result in 8.76533211 in change.",
                 10)
 
         # Test that the returned status object contains a params field with the operation's input parameters
         assert_equal(error_result["method"], "z_sendmany")
         params = error_result["params"]
-        assert_equal(params["fee"], LEGACY_DEFAULT_FEE) # default
+        assert_equal(params["fee"], fee)
         assert_equal(params["minconf"], Decimal('10')) # default
         assert_equal(params["fromaddress"], mytaddr)
         assert_equal(params["amounts"][0]["address"], myzaddr)
@@ -126,12 +125,11 @@ def run_test (self):
         myviewingkey = self.nodes[0].z_exportviewingkey(myzaddr)
         self.nodes[3].z_importviewingkey(myviewingkey, "no")
 
-        # This send will succeed.  We send two coinbase utxos totalling 20.0 less a default fee, with no change.
-        # (This tx fits within the block unpaid action limit.)
-        shieldvalue = Decimal('20.0') - LEGACY_DEFAULT_FEE
-        recipients = []
-        recipients.append({"address":myzaddr, "amount": shieldvalue})
-        myopid = self.nodes[0].z_sendmany(mytaddr, recipients, 10, LEGACY_DEFAULT_FEE, 'AllowRevealedSenders')
+        # This send will succeed. We send two coinbase utxos totalling 20.0 less fee, with no change.
+        fee = conventional_fee(4)
+        shieldvalue = Decimal('20.0') - fee
+        recipients = [{"address": myzaddr, "amount": shieldvalue}]
+        myopid = self.nodes[0].z_sendmany(mytaddr, recipients, 10, fee, 'AllowRevealedSenders')
         mytxid = wait_and_assert_operationid_status(self.nodes[0], myopid)
         self.sync_all()
 
@@ -191,9 +189,10 @@ def run_test (self):
 
         # A custom fee of 0 is okay.  Here the node will send the note value back to itself.
         # (This tx fits within the block unpaid action limit.)
-        recipients = []
-        recipients.append({"address":myzaddr, "amount": saplingvalue})
-        myopid = self.nodes[0].z_sendmany(myzaddr, recipients, 1, Decimal('0.0'))
+        fee = conventional_fee(2)
+        saplingvalue -= fee
+        recipients = [{"address": myzaddr, "amount": saplingvalue}]
+        myopid = self.nodes[0].z_sendmany(myzaddr, recipients, 1, fee)
         mytxid = wait_and_assert_operationid_status(self.nodes[0], myopid)
         self.sync_all()
         self.nodes[1].generate(1)
@@ -207,10 +206,10 @@ def run_test (self):
         check_value_pool(self.nodes[0], 'sapling', saplingvalue)
 
         # convert note to transparent funds
+        fee = conventional_fee(3)
         unshieldvalue = Decimal('10.0')
-        recipients = []
-        recipients.append({"address":mytaddr, "amount": unshieldvalue})
-        myopid = self.nodes[0].z_sendmany(myzaddr, recipients, 1, LEGACY_DEFAULT_FEE, 'AllowRevealedRecipients')
+        recipients = [{"address": mytaddr, "amount": unshieldvalue}]
+        myopid = self.nodes[0].z_sendmany(myzaddr, recipients, 1, fee, 'AllowRevealedRecipients')
         mytxid = wait_and_assert_operationid_status(self.nodes[0], myopid)
         assert(mytxid is not None)
 
@@ -219,7 +218,7 @@ def run_test (self):
         self.sync_all()
 
         # check balances
-        saplingvalue -= unshieldvalue + LEGACY_DEFAULT_FEE
+        saplingvalue -= unshieldvalue + fee
         resp = self.nodes[0].z_gettotalbalance()
         assert_equal(Decimal(resp["transparent"]), Decimal('30.0'))
         assert_equal(Decimal(resp["private"]), saplingvalue)
@@ -229,10 +228,10 @@ def run_test (self):
         # z_sendmany will return an error if there is transparent change output considered dust.
         # UTXO selection in z_sendmany sorts in ascending order, so smallest utxos are consumed first.
         # At this point in time, unspent notes all have a value of 10.0.
-        recipients = []
-        amount = Decimal('10.0') - conventional_fee(2) - Decimal('0.00000001')    # this leaves change at 1 zatoshi less than dust threshold
-        recipients.append({"address":self.nodes[0].getnewaddress(), "amount":amount })
-        myopid = self.nodes[0].z_sendmany(mytaddr, recipients, 1)
+        fee = conventional_fee(2)
+        amount = Decimal('10.0') - fee - Decimal('0.00000001')    # this leaves change at 1 zatoshi less than dust threshold
+        recipients = [{"address": self.nodes[0].getnewaddress(), "amount": amount}]
+        myopid = self.nodes[0].z_sendmany(mytaddr, recipients, 1, fee)
         wait_and_assert_operationid_status(self.nodes[0], myopid, "failed", "Insufficient funds: have 10.00, need 0.00000053 more to avoid creating invalid change output 0.00000001 (dust threshold is 0.00000054); note that coinbase outputs will not be selected if you specify ANY_TADDR, any transparent recipients are included, or if the `privacyPolicy` parameter is not set to `AllowRevealedSenders` or weaker.")
 
         # Send will fail because send amount is too big, even when including coinbase utxos
@@ -244,12 +243,11 @@ def run_test (self):
         assert_equal("Insufficient funds" in errorString, True)
 
         # z_sendmany will fail because of insufficient funds
-        recipients = []
-        recipients.append({"address":self.nodes[1].getnewaddress(), "amount":Decimal('10000.0')})
-        myopid = self.nodes[0].z_sendmany(mytaddr, recipients, 1)
+        recipients = [{"address": self.nodes[1].getnewaddress(), "amount": Decimal('10000.0')}]
+        myopid = self.nodes[0].z_sendmany(mytaddr, recipients, 1, fee)
         wait_and_assert_operationid_status(self.nodes[0], myopid, "failed", "Insufficient funds: have 10.00, need 10000.0001; note that coinbase outputs will not be selected if you specify ANY_TADDR, any transparent recipients are included, or if the `privacyPolicy` parameter is not set to `AllowRevealedSenders` or weaker.")
-        myopid = self.nodes[0].z_sendmany(myzaddr, recipients, 1, LEGACY_DEFAULT_FEE, 'AllowRevealedRecipients')
-        wait_and_assert_operationid_status(self.nodes[0], myopid, "failed", "Insufficient funds: have 9.99998, need 10000.00001; note that coinbase outputs will not be selected if you specify ANY_TADDR, any transparent recipients are included, or if the `privacyPolicy` parameter is not set to `AllowRevealedSenders` or weaker.")
+        myopid = self.nodes[0].z_sendmany(myzaddr, recipients, 1, fee, 'AllowRevealedRecipients')
+        wait_and_assert_operationid_status(self.nodes[0], myopid, "failed", "Insufficient funds: have 9.99955, need 10000.0001; note that coinbase outputs will not be selected if you specify ANY_TADDR, any transparent recipients are included, or if the `privacyPolicy` parameter is not set to `AllowRevealedSenders` or weaker.")
 
         # Send will fail because of insufficient funds unless sender uses coinbase utxos
         try:
@@ -268,7 +266,7 @@ def run_test (self):
         start_time = timeit.default_timer()
         for i in range(0,num_t_recipients):
             newtaddr = self.nodes[2].getnewaddress()
-            recipients.append({"address":newtaddr, "amount":amount_per_recipient})
+            recipients.append({"address": newtaddr, "amount": amount_per_recipient})
         elapsed = timeit.default_timer() - start_time
         print("...invoked getnewaddress() {} times in {} seconds".format(num_t_recipients, elapsed))
 
@@ -343,11 +341,13 @@ def run_test (self):
         amount_per_recipient = Decimal('0.002')
         minconf = 1
         send_amount = num_recipients * amount_per_recipient
-        custom_fee = Decimal('0.00012345')
+        custom_fee = Decimal('0.00023456')
+        assert custom_fee >= conventional_fee(4)
         zbalance = self.nodes[0].z_getbalance(myzaddr)
-        for i in range(0,num_recipients):
+        for i in range(0, num_recipients):
             newzaddr = self.nodes[2].z_getnewaddress('sapling')
-            recipients.append({"address":newzaddr, "amount":amount_per_recipient})
+            recipients.append({"address": newzaddr, "amount": amount_per_recipient})
+
         myopid = self.nodes[0].z_sendmany(myzaddr, recipients, minconf, custom_fee)
         wait_and_assert_operationid_status(self.nodes[0], myopid)
         self.sync_all()
diff --git a/qa/rpc-tests/wallet_treestate.py b/qa/rpc-tests/wallet_treestate.py
index 934c74bd60b..4cd8f8f3306 100755
--- a/qa/rpc-tests/wallet_treestate.py
+++ b/qa/rpc-tests/wallet_treestate.py
@@ -1,12 +1,12 @@
 #!/usr/bin/env python3
-# Copyright (c) 2016 The Zcash developers
+# Copyright (c) 2016-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
 from test_framework.test_framework import BitcoinTestFramework
 from test_framework.util import assert_equal, initialize_chain_clean, \
     start_nodes, connect_nodes_bi, wait_and_assert_operationid_status, \
-    get_coinbase_address, LEGACY_DEFAULT_FEE
+    get_coinbase_address
 from test_framework.zip317 import conventional_fee
 
 import time
@@ -21,7 +21,6 @@ def setup_chain(self):
     # Start nodes with -regtestshieldcoinbase to set fCoinbaseMustBeShielded to true.
     def setup_network(self, split=False):
         self.nodes = start_nodes(3, self.options.tmpdir, extra_args=[[
-            '-minrelaytxfee=0',
             '-regtestshieldcoinbase',
             '-debug=zrpc',
             '-allowdeprecated=z_getnewaddress',
@@ -45,19 +44,20 @@ def run_test (self):
         myzaddr = self.nodes[0].z_getnewaddress()
 
         # Spend coinbase utxos to create three notes of 10 ZEC minus default fee each
-        recipients = []
-        recipients.append({"address": myzaddr, "amount": Decimal('10.0') - LEGACY_DEFAULT_FEE})
-        myopid = self.nodes[0].z_sendmany(mytaddr, recipients, 1, LEGACY_DEFAULT_FEE, 'AllowRevealedSenders')
+        fee = conventional_fee(3)
+        amount = Decimal('10.0') - fee
+        recipients = [{"address": myzaddr, "amount": amount}]
+        myopid = self.nodes[0].z_sendmany(mytaddr, recipients, 1, fee, 'AllowRevealedSenders')
         wait_and_assert_operationid_status(self.nodes[0], myopid)
         self.sync_all()
         self.nodes[1].generate(1)
         self.sync_all()
-        myopid = self.nodes[0].z_sendmany(mytaddr, recipients, 1, LEGACY_DEFAULT_FEE, 'AllowRevealedSenders')
+        myopid = self.nodes[0].z_sendmany(mytaddr, recipients, 1, fee, 'AllowRevealedSenders')
         wait_and_assert_operationid_status(self.nodes[0], myopid)
         self.sync_all()
         self.nodes[1].generate(1)
         self.sync_all()
-        myopid = self.nodes[0].z_sendmany(mytaddr, recipients, 1, LEGACY_DEFAULT_FEE, 'AllowRevealedSenders')
+        myopid = self.nodes[0].z_sendmany(mytaddr, recipients, 1, fee, 'AllowRevealedSenders')
         wait_and_assert_operationid_status(self.nodes[0], myopid)
         self.sync_all()
         self.nodes[1].generate(1)
@@ -65,24 +65,25 @@ def run_test (self):
 
         # Check balance
         resp = self.nodes[0].z_getbalance(myzaddr)
-        assert_equal(Decimal(resp), (Decimal('10.0') - LEGACY_DEFAULT_FEE) * 3)
+        assert_equal(Decimal(resp), amount * 3)
 
         # We want to test a real-world situation where during the time spent creating a transaction
         # with joinsplits, other transactions containing joinsplits have been mined into new blocks,
         # which result in the treestate changing whilst creating the transaction.
 
         # Tx 1 will change the treestate while Tx 2 containing chained joinsplits is still being generated
-        recipients = []
-        recipients.append({"address": self.nodes[2].z_getnewaddress(), "amount": Decimal('10.0') - LEGACY_DEFAULT_FEE})
-        myopid = self.nodes[0].z_sendmany(mytaddr, recipients, 1, LEGACY_DEFAULT_FEE, 'AllowRevealedSenders')
+        fee = conventional_fee(3)
+        amount = Decimal('10.0') - fee
+        recipients = [{"address": self.nodes[2].z_getnewaddress(), "amount": amount}]
+        myopid = self.nodes[0].z_sendmany(mytaddr, recipients, 1, fee, 'AllowRevealedSenders')
         wait_and_assert_operationid_status(self.nodes[0], myopid)
 
         # Tx 2 will consume all three notes, which must take at least two joinsplits.  This is regardless of
         # the z_sendmany implementation because there are only two inputs per joinsplit.
         recipients = []
         recipients.append({"address": self.nodes[2].z_getnewaddress(), "amount": Decimal('18.0')})
-        recipients.append({"address": self.nodes[2].z_getnewaddress(), "amount": Decimal('12.0') - 4 * conventional_fee(3)})
-        myopid = self.nodes[0].z_sendmany(myzaddr, recipients, 1)
+        recipients.append({"address": self.nodes[2].z_getnewaddress(), "amount": Decimal('12.0') - 4*fee})
+        myopid = self.nodes[0].z_sendmany(myzaddr, recipients, 1, fee)
 
         # Wait for Tx 2 to begin executing...
         for x in range(1, 60):
diff --git a/qa/rpc-tests/wallet_unified_change.py b/qa/rpc-tests/wallet_unified_change.py
index a027363cef9..afd6561f96a 100755
--- a/qa/rpc-tests/wallet_unified_change.py
+++ b/qa/rpc-tests/wallet_unified_change.py
@@ -1,12 +1,11 @@
 #!/usr/bin/env python3
-# Copyright (c) 2022 The Zcash developers
+# Copyright (c) 2022-2024 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.test_framework import BitcoinTestFramework
 from test_framework.util import (
-    LEGACY_DEFAULT_FEE,
     NU5_BRANCH_ID,
     assert_equal,
     get_coinbase_address,
@@ -14,6 +13,8 @@
     start_nodes,
     wait_and_assert_operationid_status,
 )
+from test_framework.mininode import COIN
+from test_framework.zip317 import conventional_fee, ZIP_317_FEE
 
 # Test wallet accounts behaviour
 class WalletUnifiedChangeTest(BitcoinTestFramework):
@@ -23,7 +24,6 @@ def __init__(self):
 
     def setup_nodes(self):
         return start_nodes(self.num_nodes, self.options.tmpdir, extra_args=[[
-            '-minrelaytxfee=0',
             nuparams(NU5_BRANCH_ID, 201),
         ]] * self.num_nodes)
 
@@ -42,12 +42,14 @@ def run_test(self):
         ua1 = self.nodes[1].z_getaddressforaccount(account1)['address']
 
         # Fund both of ua0_sapling and ua0_orchard
-        recipients = [{'address': ua0_sapling, 'amount': Decimal('9.99999000')}]
-        opid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, LEGACY_DEFAULT_FEE, 'AllowRevealedSenders')
+        fee = conventional_fee(3)
+        amount = Decimal('10') - fee
+        recipients = [{'address': ua0_sapling, 'amount': amount}]
+        opid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, fee, 'AllowRevealedSenders')
         wait_and_assert_operationid_status(self.nodes[0], opid)
 
-        recipients = [{'address': ua0_orchard, 'amount': Decimal('9.99999000')}]
-        opid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, LEGACY_DEFAULT_FEE, 'AllowRevealedSenders')
+        recipients = [{'address': ua0_orchard, 'amount': amount}]
+        opid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, fee, 'AllowRevealedSenders')
         wait_and_assert_operationid_status(self.nodes[0], opid)
 
         self.sync_all()
@@ -55,18 +57,18 @@ def run_test(self):
         self.sync_all()
 
         assert_equal(
-                {'pools': {'sapling': {'valueZat': 999999000}, 'orchard': {'valueZat': 999999000}}, 'minimum_confirmations': 1},
+                {'pools': {'sapling': {'valueZat': amount * COIN}, 'orchard': {'valueZat': amount * COIN}}, 'minimum_confirmations': 1},
                 self.nodes[0].z_getbalanceforaccount(account0))
 
         # Send both amounts to ua1 in fully-shielded transactions. This will result
         # in account1 having both Sapling and Orchard balances.
 
         recipients = [{'address': ua1_sapling, 'amount': 5}]
-        opid = self.nodes[0].z_sendmany(ua0_sapling, recipients, 1, LEGACY_DEFAULT_FEE)
+        opid = self.nodes[0].z_sendmany(ua0_sapling, recipients, 1, ZIP_317_FEE)
         txid_sapling = wait_and_assert_operationid_status(self.nodes[0], opid)
 
         recipients = [{'address': ua1, 'amount': 5}]
-        opid = self.nodes[0].z_sendmany(ua0_orchard, recipients, 1, LEGACY_DEFAULT_FEE)
+        opid = self.nodes[0].z_sendmany(ua0_orchard, recipients, 1, ZIP_317_FEE)
         txid_orchard = wait_and_assert_operationid_status(self.nodes[0], opid)
 
         assert_equal(set([txid_sapling, txid_orchard]), set(self.nodes[0].getrawmempool()))
@@ -79,13 +81,15 @@ def run_test(self):
         assert_equal(1, self.nodes[0].gettransaction(txid_orchard)['confirmations'])
         assert_equal(1, self.nodes[0].gettransaction(txid_sapling)['confirmations'])
 
-        assert_equal(
-                {'pools': {'sapling': {'valueZat': 500000000}, 'orchard': {'valueZat': 500000000}}, 'minimum_confirmations': 1},
-                self.nodes[1].z_getbalanceforaccount(account1))
+        assert_equal({
+            'pools': {'sapling': {'valueZat': 500000000}, 'orchard': {'valueZat': 500000000}},
+            'minimum_confirmations': 1,
+        }, self.nodes[1].z_getbalanceforaccount(account1))
 
         # Now send sapling->sapling, generating change.
         recipients = [{'address': ua0_sapling, 'amount': Decimal('2.5')}]
-        opid = self.nodes[1].z_sendmany(ua1_sapling, recipients, 1, 0)
+        fee = conventional_fee(2)
+        opid = self.nodes[1].z_sendmany(ua1_sapling, recipients, 1, fee)
         txid_sapling = wait_and_assert_operationid_status(self.nodes[1], opid)
 
         self.sync_all()
@@ -94,23 +98,25 @@ def run_test(self):
 
         # Since this is entirely sapling->sapling, change should be returned
         # to the Sapling pool.
-        assert_equal(
-                {'pools': {'sapling': {'valueZat': 250000000}, 'orchard': {'valueZat': 500000000}}, 'minimum_confirmations': 1},
-                self.nodes[1].z_getbalanceforaccount(account1))
+        assert_equal({
+            'pools': {'sapling': {'valueZat': 250000000 - fee*COIN}, 'orchard': {'valueZat': 500000000}},
+            'minimum_confirmations': 1,
+        }, self.nodes[1].z_getbalanceforaccount(account1))
 
         # If we send from an unrestricted UA, change should still not cross
         # the pool boundary, since we can build a purely sapling->sapling tx.
         recipients = [{'address': ua0_sapling, 'amount': Decimal('1.25')}]
-        opid = self.nodes[1].z_sendmany(ua1, recipients, 1, 0)
+        opid = self.nodes[1].z_sendmany(ua1, recipients, 1, fee)
         txid_sapling = wait_and_assert_operationid_status(self.nodes[1], opid)
 
         self.sync_all()
         self.nodes[1].generate(1)
         self.sync_all()
 
-        assert_equal(
-                {'pools': {'sapling': {'valueZat': 125000000}, 'orchard': {'valueZat': 500000000}}, 'minimum_confirmations': 1},
-                self.nodes[1].z_getbalanceforaccount(account1))
+        assert_equal({
+            'pools': {'sapling': {'valueZat': 125000000 - 2*fee*COIN}, 'orchard': {'valueZat': 500000000}},
+            'minimum_confirmations': 1,
+        }, self.nodes[1].z_getbalanceforaccount(account1))
 
 if __name__ == '__main__':
     WalletUnifiedChangeTest().main()
diff --git a/qa/rpc-tests/wallet_z_sendmany.py b/qa/rpc-tests/wallet_z_sendmany.py
index b79239e2747..10ddbd0b5f5 100755
--- a/qa/rpc-tests/wallet_z_sendmany.py
+++ b/qa/rpc-tests/wallet_z_sendmany.py
@@ -1,11 +1,10 @@
 #!/usr/bin/env python3
-# Copyright (c) 2020 The Zcash developers
+# Copyright (c) 2020-2024 The Zcash developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
 from test_framework.test_framework import BitcoinTestFramework
 from test_framework.util import (
-    LEGACY_DEFAULT_FEE,
     NU5_BRANCH_ID,
     assert_equal,
     assert_greater_than,
@@ -18,6 +17,8 @@
 )
 from test_framework.authproxy import JSONRPCException
 from test_framework.mininode import COIN
+from test_framework.zip317 import conventional_fee, ZIP_317_FEE
+
 from decimal import Decimal
 
 # Test wallet address behaviour across network upgrades
@@ -28,7 +29,6 @@ def __init__(self):
 
     def setup_network(self, split=False):
         self.nodes = start_nodes(3, self.options.tmpdir, extra_args=[[
-            '-minrelaytxfee=0',
             nuparams(NU5_BRANCH_ID, 238),
             '-allowdeprecated=getnewaddress',
             '-allowdeprecated=z_getnewaddress',
@@ -41,7 +41,7 @@ def setup_network(self, split=False):
         self.is_network_split=False
         self.sync_all()
 
-    # Check we only have balances in the expected pools.
+    # Check that an account has expected balances in only the expected pools.
     # Remember that empty pools are omitted from the output.
     def _check_balance_for_rpc(self, rpcmethod, node, account, expected, minconf):
         rpc = getattr(self.nodes[node], rpcmethod)
@@ -54,6 +54,8 @@ def _check_balance_for_rpc(self, rpcmethod, node, account, expected, minconf):
         assert_equal(actual['minimum_confirmations'], minconf)
         return total_balance
 
+    # Check that an account has expected balances in only the expected pools, and that
+    # they are held only in `address`.
     def check_balance(self, node, account, address, expected, minconf=1):
         acct_balance = self._check_balance_for_rpc('z_getbalanceforaccount', node, account, expected, minconf)
         z_getbalance = self.nodes[node].z_getbalance(address, minconf)
@@ -63,86 +65,76 @@ def check_balance(self, node, account, address, expected, minconf=1):
 
     def run_test(self):
         # z_sendmany is expected to fail if tx size breaks limit
-        myzaddr = self.nodes[0].z_getnewaddress()
+        n0sapling = self.nodes[0].z_getnewaddress()
 
         recipients = []
         num_t_recipients = 1000
         num_z_recipients = 2100
         amount_per_recipient = Decimal('0.00000001')
         errorString = ''
-        for i in range(0,num_t_recipients):
+        for i in range(0, num_t_recipients):
             newtaddr = self.nodes[2].getnewaddress()
-            recipients.append({"address":newtaddr, "amount":amount_per_recipient})
-        for i in range(0,num_z_recipients):
+            recipients.append({"address": newtaddr, "amount": amount_per_recipient})
+        for i in range(0, num_z_recipients):
             newzaddr = self.nodes[2].z_getnewaddress()
-            recipients.append({"address":newzaddr, "amount":amount_per_recipient})
-
-        # Issue #2759 Workaround START
-        # HTTP connection to node 0 may fall into a state, during the few minutes it takes to process
-        # loop above to create new addresses, that when z_sendmany is called with a large amount of
-        # rpc data in recipients, the connection fails with a 'broken pipe' error.  Making a RPC call
-        # to node 0 before calling z_sendmany appears to fix this issue, perhaps putting the HTTP
-        # connection into a good state to handle a large amount of data in recipients.
-        self.nodes[0].getinfo()
-        # Issue #2759 Workaround END
+            recipients.append({"address": newzaddr, "amount": amount_per_recipient})
 
         try:
-            self.nodes[0].z_sendmany(myzaddr, recipients)
+            self.nodes[0].z_sendmany(n0sapling, recipients)
         except JSONRPCException as e:
             errorString = e.error['message']
         assert("size of raw transaction would be larger than limit" in errorString)
 
         # add zaddr to node 2
-        myzaddr = self.nodes[2].z_getnewaddress()
+        n2saddr = self.nodes[2].z_getnewaddress()
 
         # add taddr to node 2
-        mytaddr = self.nodes[2].getnewaddress()
+        n2taddr = self.nodes[2].getnewaddress()
 
         # send from node 0 to node 2 taddr
-        mytxid = self.nodes[0].sendtoaddress(mytaddr, 10.0)
+        mytxid = self.nodes[0].sendtoaddress(n2taddr, Decimal('10'))
+
         self.sync_all()
         self.nodes[0].generate(10)
         self.sync_all()
 
         # send node 2 taddr to zaddr
-        recipients = []
-        recipients.append({"address":myzaddr, "amount":7})
-        opid = self.nodes[2].z_sendmany(mytaddr, recipients, 1, LEGACY_DEFAULT_FEE, 'AllowFullyTransparent')
+        zsendmanynotevalue = Decimal('7.0')
+        fee = conventional_fee(3)
+        recipients = [{"address": n2saddr, "amount": zsendmanynotevalue}]
+        opid = self.nodes[2].z_sendmany(n2taddr, recipients, 1, fee, 'AllowFullyTransparent')
         mytxid = wait_and_assert_operationid_status(self.nodes[2], opid)
 
         self.sync_all()
-
-        # check balances
-        zsendmanynotevalue = Decimal('7.0')
-        zsendmanyfee = LEGACY_DEFAULT_FEE
-        node2sproutbalance = Decimal('50.00000000')
-        node2utxobalance = Decimal('210.00000000') - zsendmanynotevalue - zsendmanyfee
+        n2sprout_balance = Decimal('50.00000000')
 
         # check shielded balance status with getwalletinfo
         wallet_info = self.nodes[2].getwalletinfo()
         assert_equal(Decimal(wallet_info["shielded_unconfirmed_balance"]), zsendmanynotevalue)
-        assert_equal(Decimal(wallet_info["shielded_balance"]), node2sproutbalance)
+        assert_equal(Decimal(wallet_info["shielded_balance"]), n2sprout_balance)
 
         self.nodes[2].generate(10)
         self.sync_all()
 
-        assert_equal(self.nodes[2].getbalance(), node2utxobalance)
-        assert_equal(self.nodes[2].getbalance("*"), node2utxobalance)
+        n0t_balance = self.nodes[0].getbalance()
+        n2t_balance = Decimal('210.00000000') - zsendmanynotevalue - fee
+        assert_equal(Decimal(self.nodes[2].getbalance()), n2t_balance)
+        assert_equal(Decimal(self.nodes[2].getbalance("*")), n2t_balance)
 
         # check zaddr balance with z_getbalance
-        zbalance = zsendmanynotevalue
-        assert_equal(self.nodes[2].z_getbalance(myzaddr), zbalance)
+        n2saddr_balance = zsendmanynotevalue
+        assert_equal(self.nodes[2].z_getbalance(n2saddr), n2saddr_balance)
 
         # check via z_gettotalbalance
         resp = self.nodes[2].z_gettotalbalance()
-        assert_equal(Decimal(resp["transparent"]), node2utxobalance)
-        assert_equal(Decimal(resp["private"]), node2sproutbalance + zbalance)
-        assert_equal(Decimal(resp["total"]), node2utxobalance + node2sproutbalance + zbalance)
+        assert_equal(Decimal(resp["transparent"]), n2t_balance)
+        assert_equal(Decimal(resp["private"]), n2sprout_balance + n2saddr_balance)
+        assert_equal(Decimal(resp["total"]), n2t_balance + n2sprout_balance + n2saddr_balance)
 
         # check confirmed shielded balance with getwalletinfo
         wallet_info = self.nodes[2].getwalletinfo()
         assert_equal(Decimal(wallet_info["shielded_unconfirmed_balance"]), Decimal('0.0'))
-        assert_equal(Decimal(wallet_info["shielded_balance"]), node2sproutbalance + zsendmanynotevalue)
+        assert_equal(Decimal(wallet_info["shielded_balance"]), n2sprout_balance + n2saddr_balance)
 
         # there should be at least one Sapling output
         mytxdetails = self.nodes[2].getrawtransaction(mytxid, 1)
@@ -150,53 +142,51 @@ def run_test(self):
         # the Sapling output should take in all the public value
         assert_equal(mytxdetails["valueBalance"], -zsendmanynotevalue)
 
-        # send from private note to node 0 and node 2
-        node0balance = self.nodes[0].getbalance()
-        # The following assertion fails nondeterministically
-        # assert_equal(node0balance, Decimal('25.99798873'))
-        node2balance = self.nodes[2].getbalance()
-        # The following assertion might fail nondeterministically
-        # assert_equal(node2balance, Decimal('16.99799000'))
-
         # try sending with a memo to a taddr, which should fail
-        recipients = [{"address":self.nodes[0].getnewaddress(), "amount":1, "memo":"DEADBEEF"}]
-        opid = self.nodes[2].z_sendmany(myzaddr, recipients, 1, LEGACY_DEFAULT_FEE, 'AllowRevealedRecipients')
+        recipients = [{"address": self.nodes[0].getnewaddress(), "amount": Decimal('1'), "memo": "DEADBEEF"}]
+        opid = self.nodes[2].z_sendmany(n2saddr, recipients, 1, ZIP_317_FEE, 'AllowRevealedRecipients')
         wait_and_assert_operationid_status(self.nodes[2], opid, 'failed', 'Failed to build transaction: Memos cannot be sent to transparent addresses.')
 
-        recipients = []
-        recipients.append({"address":self.nodes[0].getnewaddress(), "amount":1})
-        recipients.append({"address":self.nodes[2].getnewaddress(), "amount":1.0})
+        fee = conventional_fee(4)
+        recipients = [
+            {"address": self.nodes[0].getnewaddress(), "amount": Decimal('1')},
+            {"address": self.nodes[2].getnewaddress(), "amount": Decimal('1')},
+        ];
 
-        opid = self.nodes[2].z_sendmany(myzaddr, recipients, 1, LEGACY_DEFAULT_FEE, 'AllowRevealedRecipients')
+        opid = self.nodes[2].z_sendmany(n2saddr, recipients, 1, fee, 'AllowRevealedRecipients')
         wait_and_assert_operationid_status(self.nodes[2], opid)
-        zbalance -= Decimal('2.0') + zsendmanyfee
+        n2saddr_balance -= Decimal('2') + fee
+        n0t_balance += Decimal('1')
+        n2t_balance += Decimal('1')
 
         self.sync_all()
         self.nodes[2].generate(1)
         self.sync_all()
+        n0t_balance += Decimal('10') # newly mature
 
-        node0balance += Decimal('11.0')
-        node2balance += Decimal('1.0')
-        assert_equal(Decimal(self.nodes[0].getbalance()), node0balance)
-        assert_equal(Decimal(self.nodes[0].getbalance("*")), node0balance)
-        assert_equal(Decimal(self.nodes[2].getbalance()), node2balance)
-        assert_equal(Decimal(self.nodes[2].getbalance("*")), node2balance)
+        assert_equal(Decimal(self.nodes[0].getbalance()), n0t_balance)
+        assert_equal(Decimal(self.nodes[0].getbalance("*")), n0t_balance)
+        assert_equal(Decimal(self.nodes[2].getbalance()), n2t_balance)
+        assert_equal(Decimal(self.nodes[2].getbalance("*")), n2t_balance)
+        assert_equal(self.nodes[2].z_getbalance(n2saddr), n2saddr_balance)
 
-        # Get a new unified account on node 2 & generate a UA
-        n0account0 = self.nodes[0].z_getnewaccount()['account']
-        n0ua0 = self.nodes[0].z_getaddressforaccount(n0account0)['address']
+        # Get a new unified account on node 0 & generate a UA
+        n0account = self.nodes[0].z_getnewaccount()['account']
+        n0ua0 = self.nodes[0].z_getaddressforaccount(n0account)['address']
+        n0ua0_balance = Decimal('0')
+        self.check_balance(0, 0, n0ua0, {})
 
         # Prepare to fund the UA from coinbase
         source = get_coinbase_address(self.nodes[2])
-        recipients = []
-        recipients.append({"address":n0ua0, "amount":10})
+        fee = conventional_fee(3)
+        recipients = [{"address": n0ua0, "amount": Decimal('10') - fee}]
 
         # If we attempt to spend with the default privacy policy, z_sendmany
         # fails because it needs to spend transparent coins in a transaction
         # involving a Unified Address.
         unified_address_msg = 'Could not send to a shielded receiver of a unified address without spending funds from a different pool, which would reveal transaction amounts. THIS MAY AFFECT YOUR PRIVACY. Resubmit with the `privacyPolicy` parameter set to `AllowRevealedAmounts` or weaker if you wish to allow this transaction to proceed anyway.'
         revealed_senders_msg = 'Insufficient funds: have 0.00, need 10.00; note that coinbase outputs will not be selected if you specify ANY_TADDR, any transparent recipients are included, or if the `privacyPolicy` parameter is not set to `AllowRevealedSenders` or weaker.'
-        opid = self.nodes[2].z_sendmany(source, recipients, 1, 0)
+        opid = self.nodes[2].z_sendmany(source, recipients, 1, fee)
         wait_and_assert_operationid_status(self.nodes[2], opid, 'failed', unified_address_msg)
 
         # We can't create a transaction with an unknown privacy policy.
@@ -204,7 +194,7 @@ def run_test(self):
             JSONRPCException,
             'Unknown privacy policy name \'ZcashIsAwesome\'',
             self.nodes[2].z_sendmany,
-            source, recipients, 1, 0, 'ZcashIsAwesome')
+            source, recipients, 1, fee, 'ZcashIsAwesome')
 
         # If we set any policy that does not include AllowRevealedSenders,
         # z_sendmany also fails.
@@ -213,31 +203,33 @@ def run_test(self):
             ('AllowRevealedAmounts', revealed_senders_msg),
             ('AllowRevealedRecipients', revealed_senders_msg),
         ]:
-            opid = self.nodes[2].z_sendmany(source, recipients, 1, 0, policy)
+            opid = self.nodes[2].z_sendmany(source, recipients, 1, fee, policy)
             wait_and_assert_operationid_status(self.nodes[2], opid, 'failed', msg)
 
         # By setting the correct policy, we can create the transaction.
-        opid = self.nodes[2].z_sendmany(source, recipients, 1, 0, 'AllowRevealedSenders')
+        opid = self.nodes[2].z_sendmany(source, recipients, 1, fee, 'AllowRevealedSenders')
         wait_and_assert_operationid_status(self.nodes[2], opid)
+        n2t_balance -= Decimal('10.0')
+        n0ua0_balance += Decimal('10') - fee
 
+        self.sync_all()
         self.nodes[2].generate(1)
         self.sync_all()
+        n0t_balance += Decimal('10.0') # newly mature
 
-        node2balance -= Decimal('10.0')
-        node0balance += Decimal('10.0')
-        assert_equal(Decimal(self.nodes[2].getbalance()), node2balance)
-        assert_equal(Decimal(self.nodes[0].getbalance()), node0balance)
-        self.check_balance(0, 0, n0ua0, {'sapling': 10})
+        self.check_balance(0, 0, n0ua0, {'sapling': n0ua0_balance})
+        assert_equal(Decimal(self.nodes[0].getbalance()), n0t_balance)
+        assert_equal(Decimal(self.nodes[2].getbalance()), n2t_balance)
 
         # Send some funds to a specific legacy taddr that we can spend from
-        recipients = []
-        recipients.append({"address":mytaddr, "amount":5})
+        fee = conventional_fee(3)
+        recipients = [{"address": n2taddr, "amount": Decimal('5')}]
 
         # If we attempt to spend with the default privacy policy, z_sendmany
         # returns an error because it needs to create a transparent recipient in
         # a transaction involving a Unified Address.
         revealed_recipients_msg = "This transaction would have transparent recipients, which is not enabled by default because it will publicly reveal transaction recipients and amounts. THIS MAY AFFECT YOUR PRIVACY. Resubmit with the `privacyPolicy` parameter set to `AllowRevealedRecipients` or weaker if you wish to allow this transaction to proceed anyway."
-        opid = self.nodes[0].z_sendmany(n0ua0, recipients, 1, 0)
+        opid = self.nodes[0].z_sendmany(n0ua0, recipients, 1, fee)
         wait_and_assert_operationid_status(self.nodes[0], opid, 'failed', revealed_recipients_msg)
 
         # If we set any policy that does not include AllowRevealedRecipients,
@@ -248,60 +240,68 @@ def run_test(self):
             'AllowRevealedSenders',
             'AllowLinkingAccountAddresses',
         ]:
-            opid = self.nodes[0].z_sendmany(n0ua0, recipients, 1, 0, policy)
+            opid = self.nodes[0].z_sendmany(n0ua0, recipients, 1, fee, policy)
             wait_and_assert_operationid_status(self.nodes[0], opid, 'failed', revealed_recipients_msg)
 
         # By setting the correct policy, we can create the transaction.
-        opid = self.nodes[0].z_sendmany(n0ua0, recipients, 1, 0, 'AllowRevealedRecipients')
+        opid = self.nodes[0].z_sendmany(n0ua0, recipients, 1, fee, 'AllowRevealedRecipients')
         wait_and_assert_operationid_status(self.nodes[0], opid)
+        n2t_balance += Decimal('5')
+        n0ua0_balance -= Decimal('5') + fee
 
+        self.sync_all()
         self.nodes[0].generate(1)
         self.sync_all()
 
-        node2balance += Decimal('5.0')
-        self.check_balance(0, 0, n0ua0, {'sapling': 5})
-        assert_equal(Decimal(self.nodes[2].getbalance()), node2balance)
+        self.check_balance(0, 0, n0ua0, {'sapling': n0ua0_balance})
+        assert_equal(Decimal(self.nodes[2].getbalance()), n2t_balance)
 
         # Send some funds to a legacy sapling address that we can spend from
-        recipients = []
-        recipients.append({"address":myzaddr, "amount":3})
-        opid = self.nodes[0].z_sendmany(n0ua0, recipients, 1, 0)
+        fee = conventional_fee(2)
+        recipients = [{"address": n2saddr, "amount": Decimal('3')}]
+        opid = self.nodes[0].z_sendmany(n0ua0, recipients, 1, fee)
         wait_and_assert_operationid_status(self.nodes[0], opid)
+        n2saddr_balance += Decimal('3')
+        n0ua0_balance -= Decimal('3') + fee
 
+        self.sync_all()
         self.nodes[0].generate(1)
         self.sync_all()
 
-        zbalance += Decimal('3.0')
-        self.check_balance(0, 0, n0ua0, {'sapling': 2})
-        assert_equal(Decimal(self.nodes[2].z_getbalance(myzaddr)), zbalance)
+        self.check_balance(0, 0, n0ua0, {'sapling': n0ua0_balance})
+        assert_equal(self.nodes[2].z_getbalance(n2saddr), n2saddr_balance)
 
         # Send funds back from the legacy taddr to the UA. This requires
         # AllowRevealedSenders, but we can also use any weaker policy that
         # includes it.
-        recipients = []
-        recipients.append({"address":n0ua0, "amount":4})
-        opid = self.nodes[2].z_sendmany(mytaddr, recipients, 1, 0, 'AllowFullyTransparent')
+        fee = conventional_fee(3)
+        recipients = [{"address": n0ua0, "amount": Decimal('4')}]
+        opid = self.nodes[2].z_sendmany(n2taddr, recipients, 1, fee, 'AllowFullyTransparent')
         wait_and_assert_operationid_status(self.nodes[2], opid)
+        n0ua0_balance += Decimal('4')
+        n2t_balance -= Decimal('4') + fee
 
+        self.sync_all()
         self.nodes[2].generate(1)
         self.sync_all()
 
-        node2balance -= Decimal('4.0')
-        self.check_balance(0, 0, n0ua0, {'sapling': 6})
-        assert_equal(Decimal(self.nodes[2].getbalance()), node2balance)
+        self.check_balance(0, 0, n0ua0, {'sapling': n0ua0_balance})
+        assert_equal(Decimal(self.nodes[2].getbalance()), n2t_balance)
 
         # Send funds back from the legacy zaddr to the UA
-        recipients = []
-        recipients.append({"address":n0ua0, "amount":2})
-        opid = self.nodes[2].z_sendmany(myzaddr, recipients, 1, 0)
+        fee = conventional_fee(2)
+        recipients = [{"address": n0ua0, "amount": Decimal('2')}]
+        opid = self.nodes[2].z_sendmany(n2saddr, recipients, 1, fee)
         wait_and_assert_operationid_status(self.nodes[2], opid)
+        n0ua0_balance += Decimal('2')
+        n2saddr_balance -= Decimal('2') + fee
 
+        self.sync_all()
         self.nodes[2].generate(1)
         self.sync_all()
 
-        zbalance -= Decimal('2.0')
-        self.check_balance(0, 0, n0ua0, {'sapling': 8})
-        assert_equal(Decimal(self.nodes[2].z_getbalance(myzaddr)), zbalance)
+        self.check_balance(0, 0, n0ua0, {'sapling': n0ua0_balance})
+        assert_equal(self.nodes[2].z_getbalance(n2saddr), n2saddr_balance)
 
         #
         # Test that z_sendmany avoids UA linkability unless we allow it.
@@ -317,26 +317,34 @@ def run_test(self):
             taddr = self.nodes[1].z_listunifiedreceivers(ua)['p2pkh']
             self.nodes[0].sendtoaddress(taddr, 2)
 
+        n0sapling_balance = n0ua0_balance
+        n1ua0_balance = Decimal('2')
+        n1ua1_balance = Decimal('2')
+        n1t_balance = n1ua0_balance + n1ua1_balance
+
         self.sync_all()
         self.nodes[2].generate(1)
         self.sync_all()
 
+        self.check_balance(0, 0, n0ua0, {'sapling': n0sapling_balance})
+
         # The account should see all funds.
-        assert_equal(
-            self.nodes[1].z_getbalanceforaccount(n1account)['pools'],
-            {'transparent': {'valueZat': 4 * COIN}},
-        )
+        assert_equal(self.nodes[1].z_getbalanceforaccount(n1account)['pools'], {
+            'transparent': {'valueZat': n1t_balance * COIN},
+        })
 
         # The addresses should see only the transparent funds sent to them.
-        assert_equal(self.nodes[1].z_getbalance(n1ua0), 2)
-        assert_equal(self.nodes[1].z_getbalance(n1ua1), 2)
+        assert_equal(Decimal(self.nodes[1].z_getbalance(n1ua0)), n1ua0_balance)
+        assert_equal(Decimal(self.nodes[1].z_getbalance(n1ua1)), n1ua1_balance)
 
-        # If we try to send 3 ZEC from n1ua0, it will fail with too-few funds.
-        recipients = [{"address":n0ua0, "amount":3}]
+        # If we try to send 3 ZEC less fee from n1ua0, it will fail with insufficient funds.
+        fee = conventional_fee(4)
+        amount = Decimal('3') - fee
+        recipients = [{"address": n0ua0, "amount": amount}]
         linked_addrs_with_coinbase_note_msg = 'Insufficient funds: have 0.00, need 3.00; note that coinbase outputs will not be selected if you specify ANY_TADDR, any transparent recipients are included, or if the `privacyPolicy` parameter is not set to `AllowRevealedSenders` or weaker. (This transaction may require selecting transparent coins that were sent to multiple addresses, which is not enabled by default because it would create a public link between those addresses. THIS MAY AFFECT YOUR PRIVACY. Resubmit with the `privacyPolicy` parameter set to `AllowLinkingAccountAddresses` or weaker if you wish to allow this transaction to proceed anyway.)'
         linked_addrs_without_coinbase_note_msg = 'Insufficient funds: have 2.00, need 3.00. (This transaction may require selecting transparent coins that were sent to multiple addresses, which is not enabled by default because it would create a public link between those addresses. THIS MAY AFFECT YOUR PRIVACY. Resubmit with the `privacyPolicy` parameter set to `AllowLinkingAccountAddresses` or weaker if you wish to allow this transaction to proceed anyway.)'
         revealed_amounts_msg = 'Could not send to a shielded receiver of a unified address without spending funds from a different pool, which would reveal transaction amounts. THIS MAY AFFECT YOUR PRIVACY. Resubmit with the `privacyPolicy` parameter set to `AllowRevealedAmounts` or weaker if you wish to allow this transaction to proceed anyway.'
-        opid = self.nodes[1].z_sendmany(n1ua0, recipients, 1, 0)
+        opid = self.nodes[1].z_sendmany(n1ua0, recipients, 1, fee)
         wait_and_assert_operationid_status(self.nodes[1], opid, 'failed', revealed_amounts_msg)
 
         # If we try it again with any policy that is too strong, it also fails.
@@ -347,45 +355,50 @@ def run_test(self):
             ('AllowRevealedSenders', linked_addrs_without_coinbase_note_msg),
             ('AllowFullyTransparent', linked_addrs_without_coinbase_note_msg),
         ]:
-            opid = self.nodes[1].z_sendmany(n1ua0, recipients, 1, 0, policy)
+            opid = self.nodes[1].z_sendmany(n1ua0, recipients, 1, fee, policy)
             wait_and_assert_operationid_status(self.nodes[1], opid, 'failed', msg)
 
         # If we try to send just a bit less than we have, it will fail, complaining about dust
-        opid = self.nodes[1].z_sendmany(n1ua0, [{"address":n0ua0, "amount":3.9999999}], 1, 0, 'AllowLinkingAccountAddresses')
+        opid = self.nodes[1].z_sendmany(n1ua0,
+                                        [{"address": n0ua0, "amount": Decimal('3.9999999') - fee}],
+                                        1, fee, 'AllowLinkingAccountAddresses')
         wait_and_assert_operationid_status(self.nodes[1], opid, 'failed', 'Insufficient funds: have 4.00, need 0.00000044 more to avoid creating invalid change output 0.0000001 (dust threshold is 0.00000054).')
 
-        # Once we provide a sufficiently-weak policy, the transaction succeeds.
-        opid = self.nodes[1].z_sendmany(n1ua0, recipients, 1, 0, 'AllowLinkingAccountAddresses')
+        # Once we provide a sufficiently weak policy, the transaction succeeds.
+        opid = self.nodes[1].z_sendmany(n1ua0, recipients, 1, fee, 'AllowLinkingAccountAddresses')
         wait_and_assert_operationid_status(self.nodes[1], opid)
+        n0ua0_balance += amount
+        # Change should be sent to the Sapling change address (because NU5 is not active).
+        n1sapling_balance = n1t_balance - amount - fee
+        del n0sapling_balance
+        del n1t_balance
 
         self.sync_all()
         self.nodes[2].generate(1)
         self.sync_all()
 
-        # The account should see the remaining funds, and they should have been
-        # sent to the Sapling change address (because NU5 is not active).
-        assert_equal(
-            self.nodes[1].z_getbalanceforaccount(n1account)['pools'],
-            {'sapling': {'valueZat': 1 * COIN}},
-        )
+        self.check_balance(0, 0, n0ua0, {'sapling': n0ua0_balance})
+        assert_equal(self.nodes[1].z_getbalanceforaccount(n1account)['pools'], {
+            'sapling': {'valueZat': n1sapling_balance * COIN},
+        })
 
-        # The addresses should both show the same balance, as they both show the
-        # Sapling balance.
-        assert_equal(self.nodes[1].z_getbalance(n1ua0), 1)
-        assert_equal(self.nodes[1].z_getbalance(n1ua1), 1)
+        # z_getbalance behaves inconsistently between transparent and shielded
+        # addresses: for a shielded address it gives the account balance.
+        assert_equal(Decimal(self.nodes[1].z_getbalance(n1ua0)), n1sapling_balance)
+        assert_equal(Decimal(self.nodes[1].z_getbalance(n1ua1)), n1sapling_balance)
 
         #
         # Test Orchard-only UA before NU5
         #
 
-        n0orchard_only = self.nodes[0].z_getaddressforaccount(n0account0, ["orchard"])['address']
-        recipients = [{"address":n0orchard_only, "amount":1}]
+        n0orchard_only = self.nodes[0].z_getaddressforaccount(n0account, ["orchard"])['address']
+        recipients = [{"address": n0orchard_only, "amount": Decimal('1')}]
         for (policy, msg) in [
             ('FullPrivacy', 'Could not send to a shielded receiver of a unified address without spending funds from a different pool, which would reveal transaction amounts. THIS MAY AFFECT YOUR PRIVACY. Resubmit with the `privacyPolicy` parameter set to `AllowRevealedAmounts` or weaker if you wish to allow this transaction to proceed anyway.'),
             ('AllowRevealedAmounts', 'This transaction would send to a transparent receiver of a unified address, which is not enabled by default because it will publicly reveal transaction recipients and amounts. THIS MAY AFFECT YOUR PRIVACY. Resubmit with the `privacyPolicy` parameter set to `AllowRevealedRecipients` or weaker if you wish to allow this transaction to proceed anyway.'),
             ('AllowRevealedRecipients', 'Could not send to an Orchard-only receiver despite a lax privacy policy, because NU5 has not been activated yet.'),
         ]:
-            opid = self.nodes[1].z_sendmany(n1ua0, recipients, 1, 0, policy)
+            opid = self.nodes[1].z_sendmany(n1ua0, recipients, 1, fee, policy)
             wait_and_assert_operationid_status(self.nodes[1], opid, 'failed', msg)
 
         #
@@ -394,26 +407,34 @@ def run_test(self):
 
         # Send some legacy transparent funds to n1ua0, creating Sapling outputs.
         source = get_coinbase_address(self.nodes[2])
-        recipients = [{"address":n1ua0, "amount":10}]
+        fee = conventional_fee(3)
+        recipients = [{"address": n1ua0, "amount": Decimal('10') - fee}]
         # This requires the AllowRevealedSenders policy, but we specify only AllowRevealedAmounts...
-        opid = self.nodes[2].z_sendmany(source, recipients, 1, 0, 'AllowRevealedAmounts')
+        opid = self.nodes[2].z_sendmany(source, recipients, 1, fee, 'AllowRevealedAmounts')
         wait_and_assert_operationid_status(self.nodes[2], opid, 'failed', revealed_senders_msg)
         # ... which we can always override with the NoPrivacy policy.
-        opid = self.nodes[2].z_sendmany(source, recipients, 1, 0, 'NoPrivacy')
+        opid = self.nodes[2].z_sendmany(source, recipients, 1, fee, 'NoPrivacy')
         wait_and_assert_operationid_status(self.nodes[2], opid)
+        n1sapling_balance += Decimal('10') - fee
 
         self.sync_all()
         self.nodes[2].generate(1)
         self.sync_all()
 
+        assert_equal(self.nodes[1].z_getbalanceforaccount(n1account)['pools'], {
+            'sapling': {'valueZat': n1sapling_balance * COIN},
+        })
+
         # Send some funds from node 1's account to a transparent address.
-        recipients = [{"address":mytaddr, "amount":5}]
+        fee = conventional_fee(3)
+        recipients = [{"address": n2taddr, "amount": Decimal('5')}]
         # This requires the AllowRevealedRecipients policy...
-        opid = self.nodes[1].z_sendmany(n1ua0, recipients, 1, 0)
+        opid = self.nodes[1].z_sendmany(n1ua0, recipients, 1, fee)
         wait_and_assert_operationid_status(self.nodes[1], opid, 'failed', revealed_recipients_msg)
         # ... which we can always override with the NoPrivacy policy.
-        opid = self.nodes[1].z_sendmany(n1ua0, recipients, 1, 0, 'NoPrivacy')
+        opid = self.nodes[1].z_sendmany(n1ua0, recipients, 1, fee, 'NoPrivacy')
         wait_and_assert_operationid_status(self.nodes[1], opid)
+        n1sapling_balance -= Decimal('5') + fee
 
         # Activate NU5
 
@@ -421,80 +442,94 @@ def run_test(self):
         self.nodes[1].generate(10)
         self.sync_all()
 
+        assert_equal(self.nodes[1].z_getbalanceforaccount(n1account)['pools'], {
+            'sapling': {'valueZat': n1sapling_balance * COIN},
+        })
+
         #
         # Test sending Sprout funds to Orchard-only UA
         #
 
         sproutAddr = self.nodes[2].listaddresses()[0]['sprout']['addresses'][0]
-        recipients = [{"address":n0orchard_only, "amount":100}]
+        recipients = [{"address": n0orchard_only, "amount": Decimal('100')}]
         for (policy, msg) in [
             ('FullPrivacy', 'Could not send to a shielded receiver of a unified address without spending funds from a different pool, which would reveal transaction amounts. THIS MAY AFFECT YOUR PRIVACY. Resubmit with the `privacyPolicy` parameter set to `AllowRevealedAmounts` or weaker if you wish to allow this transaction to proceed anyway.'),
             ('AllowRevealedAmounts', 'This transaction would send to a transparent receiver of a unified address, which is not enabled by default because it will publicly reveal transaction recipients and amounts. THIS MAY AFFECT YOUR PRIVACY. Resubmit with the `privacyPolicy` parameter set to `AllowRevealedRecipients` or weaker if you wish to allow this transaction to proceed anyway.'),
             ('AllowRevealedRecipients', 'Could not send to an Orchard-only receiver despite a lax privacy policy, because you are sending from the Sprout pool and there is no transaction version that supports both Sprout and Orchard.'),
+            ('NoPrivacy', 'Could not send to an Orchard-only receiver despite a lax privacy policy, because you are sending from the Sprout pool and there is no transaction version that supports both Sprout and Orchard.'),
         ]:
-            opid = self.nodes[2].z_sendmany(sproutAddr, recipients, 1, 0, policy)
+            opid = self.nodes[2].z_sendmany(sproutAddr, recipients, 1, ZIP_317_FEE, policy)
             wait_and_assert_operationid_status(self.nodes[2], opid, 'failed', msg)
 
         #
         # Test AllowRevealedAmounts policy
         #
 
-        assert_equal(
-                {'pools': {'sapling': {'valueZat': 600000000}}, 'minimum_confirmations': 1},
-                self.nodes[1].z_getbalanceforaccount(n1account))
-
-        # Sending some funds to the Orchard pool in n0account0 ...
-        n0ua1 = self.nodes[0].z_getaddressforaccount(n0account0, ["orchard"])['address']
-        recipients = [{"address":n0ua1, "amount": 6}]
+        # Sending some funds to the Orchard pool in n0account ...
+        n0ua1 = self.nodes[0].z_getaddressforaccount(n0account, ["orchard"])['address']
+        fee = conventional_fee(4)
+        recipients = [{"address": n0ua1, "amount": Decimal('5')}]
 
         # Should fail under default and 'FullPrivacy' policies ...
         revealed_amounts_msg = 'Could not send to a shielded receiver of a unified address without spending funds from a different pool, which would reveal transaction amounts. THIS MAY AFFECT YOUR PRIVACY. Resubmit with the `privacyPolicy` parameter set to `AllowRevealedAmounts` or weaker if you wish to allow this transaction to proceed anyway.'
-        opid = self.nodes[1].z_sendmany(n1ua0, recipients, 1, 0)
+        opid = self.nodes[1].z_sendmany(n1ua0, recipients, 1, fee)
         wait_and_assert_operationid_status(self.nodes[1], opid, 'failed', revealed_amounts_msg)
 
-        opid = self.nodes[1].z_sendmany(n1ua0, recipients, 1, 0, 'FullPrivacy')
+        opid = self.nodes[1].z_sendmany(n1ua0, recipients, 1, fee, 'FullPrivacy')
         wait_and_assert_operationid_status(self.nodes[1], opid, 'failed', revealed_amounts_msg)
 
-        # Should succeed under 'AllowRevealedAmounts'
-        opid = self.nodes[1].z_sendmany(n1ua0, recipients, 1, 0, 'AllowRevealedAmounts')
+        # Should succeed under 'AllowRevealedAmounts'. The change will go to Orchard.
+        opid = self.nodes[1].z_sendmany(n1ua0, recipients, 1, fee, 'AllowRevealedAmounts')
         wait_and_assert_operationid_status(self.nodes[1], opid)
+        n0sapling_balance = n0ua0_balance
+        n0orchard_balance = Decimal('5')
+        n1orchard_balance = n1sapling_balance - Decimal('5') - fee
+        del n0ua0_balance
+        del n1sapling_balance
 
         self.sync_all()
         self.nodes[1].generate(1)
         self.sync_all()
 
-        assert_equal(
-                {'pools': {'sapling': {'valueZat': 1100000000}, 'orchard': {'valueZat': 600000000}}, 'minimum_confirmations': 1},
-                self.nodes[0].z_getbalanceforaccount(n0account0))
+        assert_equal(self.nodes[0].z_getbalanceforaccount(n0account)['pools'], {
+            'sapling': {'valueZat': n0sapling_balance * COIN},
+            'orchard': {'valueZat': n0orchard_balance * COIN},
+        })
+        assert_equal(self.nodes[1].z_getbalanceforaccount(n1account)['pools'], {
+            'orchard': {'valueZat': n1orchard_balance * COIN},
+        })
 
         # A total that requires selecting from both pools should fail under default and
         # FullPrivacy policies...
 
-        recipients = [{"address":n1ua0, "amount": 15}]
-        opid = self.nodes[0].z_sendmany(n0ua0, recipients, 1, 0)
+        fee = conventional_fee(3)
+        recipients = [{"address": n1ua0, "amount": Decimal('15')}]
+        opid = self.nodes[0].z_sendmany(n0ua0, recipients, 1, fee)
         wait_and_assert_operationid_status(self.nodes[0], opid, 'failed', revealed_amounts_msg)
 
-        opid = self.nodes[0].z_sendmany(n0ua0, recipients, 1, 0, 'FullPrivacy')
+        opid = self.nodes[0].z_sendmany(n0ua0, recipients, 1, fee, 'FullPrivacy')
         wait_and_assert_operationid_status(self.nodes[0], opid, 'failed', revealed_amounts_msg)
 
         # Should succeed under 'AllowRevealedAmounts'
-        opid = self.nodes[0].z_sendmany(n0ua0, recipients, 1, 0, 'AllowRevealedAmounts')
+        # All funds should be received to the Orchard pool, and all change should
+        # be optimistically shielded.
+        fee = conventional_fee(6)
+        opid = self.nodes[0].z_sendmany(n0ua0, recipients, 1, fee, 'AllowRevealedAmounts')
         wait_and_assert_operationid_status(self.nodes[0], opid)
+        n0orchard_balance += n0sapling_balance - Decimal('15') - fee
+        n1orchard_balance += Decimal('15')
+        del n0sapling_balance
 
         self.sync_all()
         self.nodes[1].generate(1)
         self.sync_all()
 
-        # All funds should be received to the Orchard pool
-        assert_equal(
-                {'pools': {'orchard': {'valueZat': 1500000000}}, 'minimum_confirmations': 1},
-                self.nodes[1].z_getbalanceforaccount(n1account))
-
-        # And all change should be optimistically shielded
-        assert_equal(
-                {'pools': {'orchard': {'valueZat': 200000000}}, 'minimum_confirmations': 1},
-                self.nodes[0].z_getbalanceforaccount(n0account0))
-
+        assert_equal(self.nodes[0].z_getbalanceforaccount(n0account)['pools'], {
+            'orchard': {'valueZat': n0orchard_balance * COIN},
+        })
+        assert_equal(self.nodes[1].z_getbalanceforaccount(n1account)['pools'], {
+            'orchard': {'valueZat': n1orchard_balance * COIN},
+        })
 
         self.sync_all()
         self.nodes[1].generate(1)
@@ -504,15 +539,25 @@ def run_test(self):
         # Test transparent change
         #
 
-        recipients = [{"address":n0ua1, "amount": 4}]
+        fee = conventional_fee(3)
+        recipients = [{"address": n0ua1, "amount": Decimal('4')}]
         # Should fail because this generates transparent change, but we don’t have
         # `AllowRevealedRecipients`
-        opid = self.nodes[2].z_sendmany(mytaddr, recipients, 1, 0, 'AllowRevealedSenders')
+        opid = self.nodes[2].z_sendmany(n2taddr, recipients, 1, fee, 'AllowRevealedSenders')
         wait_and_assert_operationid_status(self.nodes[2], opid, 'failed', "This transaction would have transparent change, which is not enabled by default because it will publicly reveal the change address and amounts. THIS MAY AFFECT YOUR PRIVACY. Resubmit with the `privacyPolicy` parameter set to `AllowRevealedRecipients` or weaker if you wish to allow this transaction to proceed anyway.")
 
         # Should succeed once we include `AllowRevealedRecipients`
-        opid = self.nodes[2].z_sendmany(mytaddr, recipients, 1, 0, 'AllowFullyTransparent')
+        opid = self.nodes[2].z_sendmany(n2taddr, recipients, 1, fee, 'AllowFullyTransparent')
         wait_and_assert_operationid_status(self.nodes[2], opid)
+        n0orchard_balance += Decimal('4')
+
+        self.sync_all()
+        self.nodes[1].generate(1)
+        self.sync_all()
+
+        assert_equal(self.nodes[0].z_getbalanceforaccount(n0account)['pools'], {
+            'orchard': {'valueZat': n0orchard_balance * COIN},
+        })
 
 if __name__ == '__main__':
     WalletZSendmanyTest().main()
diff --git a/qa/rpc-tests/wallet_zip317_default.py b/qa/rpc-tests/wallet_zip317_default.py
new file mode 100755
index 00000000000..60b8948c0ea
--- /dev/null
+++ b/qa/rpc-tests/wallet_zip317_default.py
@@ -0,0 +1,97 @@
+#!/usr/bin/env python3
+# Copyright (c) 2022-2024 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.test_framework import BitcoinTestFramework
+from test_framework.mininode import COIN
+from test_framework.util import (
+    CANOPY_BRANCH_ID,
+    NU5_BRANCH_ID,
+    assert_equal,
+    assert_raises_message,
+    get_coinbase_address,
+    nuparams,
+    start_nodes,
+    wait_and_assert_operationid_status,
+)
+from test_framework.zip317 import conventional_fee, ZIP_317_FEE
+
+# Regression test for https://github.com/zcash/zcash/issues/6956 .
+class WalletZip317DefaultTest(BitcoinTestFramework):
+    def __init__(self):
+        super().__init__()
+        self.num_nodes = 2
+
+    def setup_nodes(self):
+        return start_nodes(self.num_nodes, self.options.tmpdir, extra_args=[[
+            nuparams(CANOPY_BRANCH_ID, 205),
+            nuparams(NU5_BRANCH_ID, 206),
+            '-allowdeprecated=getnewaddress',
+            '-allowdeprecated=z_getnewaddress',
+            '-regtestwalletsetbestchaineveryblock',
+        ]] * self.num_nodes)
+
+    def run_test(self):
+        # Sanity-check the test harness.
+        assert_equal(self.nodes[0].getblockcount(), 200)
+
+        # Create an account with funds in the Sapling receiver.
+        acct0 = self.nodes[0].z_getnewaccount()['account']
+        ua0 = self.nodes[0].z_getaddressforaccount(acct0, ['sapling', 'orchard'])['address']
+
+        coinbase_fee = conventional_fee(3)
+        balance0 = Decimal('10') - coinbase_fee
+        recipients = [{"address": ua0, "amount": balance0}]
+        opid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, coinbase_fee, 'AllowRevealedSenders')
+        wait_and_assert_operationid_status(self.nodes[0], opid)
+
+        # Mine the tx & activate NU5.
+        self.sync_all()
+        self.nodes[0].generate(10)
+        self.sync_all()
+
+        assert_equal(
+            {'pools': {'sapling': {'valueZat': balance0 * COIN}}, 'minimum_confirmations': 1},
+            self.nodes[0].z_getbalanceforaccount(acct0),
+        )
+
+        acct1 = self.nodes[1].z_getnewaccount()['account']
+        ua1 = self.nodes[1].z_getaddressforaccount(acct1, ['orchard'])['address']
+
+        # The next z_sendmany call fails when passed `None`/`ZIP_317_FEE` because it
+        # calculates a fee that is too low.
+        # https://github.com/zcash/zcash/issues/6956
+        recipients = [{"address": ua1, "amount": Decimal('1')}]
+        opid = self.nodes[0].z_sendmany(ua0, recipients, 1, ZIP_317_FEE, 'AllowRevealedAmounts')
+        # The buggy behaviour.
+        assert_raises_message(AssertionError,
+                              "Transaction commit failed:: tx unpaid action limit exceeded: 1 action(s) exceeds limit of 0",
+                              wait_and_assert_operationid_status, self.nodes[0], opid)
+
+        # If we pass `fee` instead of `None`, it succeeds.
+        fee = conventional_fee(4)
+        opid = self.nodes[0].z_sendmany(ua0, recipients, 1, fee, 'AllowRevealedAmounts')
+        wait_and_assert_operationid_status(self.nodes[0], opid)
+
+        self.sync_all()
+        self.nodes[0].generate(1)
+        self.sync_all()
+
+        # The nodes have the expected split of funds.
+        balance0 -= Decimal('1') + fee
+        assert_equal(
+            {'pools': {'orchard': {'valueZat': balance0 * COIN}}, 'minimum_confirmations': 1},
+            self.nodes[0].z_getbalanceforaccount(acct0),
+        )
+        assert_equal(
+            {'pools': {'orchard': {'valueZat': 1_0000_0000}}, 'minimum_confirmations': 1},
+            self.nodes[1].z_getbalanceforaccount(acct1),
+        )
+
+        # TODO: also test the case in `wallet_isfromme.py`.
+
+if __name__ == '__main__':
+    WalletZip317DefaultTest().main()
diff --git a/qa/rpc-tests/zkey_import_export.py b/qa/rpc-tests/zkey_import_export.py
index 13d7d1c0f0d..b60fe65e5fb 100755
--- a/qa/rpc-tests/zkey_import_export.py
+++ b/qa/rpc-tests/zkey_import_export.py
@@ -1,21 +1,23 @@
 #!/usr/bin/env python3
-# Copyright (c) 2017 The Zcash developers
+# Copyright (c) 2017-2024 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.test_framework import BitcoinTestFramework
-from test_framework.util import assert_equal, assert_greater_than, start_nodes,\
-    initialize_chain_clean, connect_nodes_bi, wait_and_assert_operationid_status, \
-    LEGACY_DEFAULT_FEE
+from test_framework.authproxy import JSONRPCException
+from test_framework.util import (
+    assert_equal, assert_greater_than, assert_raises_message, start_nodes,
+    initialize_chain_clean, connect_nodes_bi, wait_and_assert_operationid_status,
+)
+from test_framework.zip317 import conventional_fee
+
 from functools import reduce
 import logging
 import sys
 
 logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.INFO, stream=sys.stdout)
 
-fee = LEGACY_DEFAULT_FEE # constant (but can be changed within reason)
-
 class ZkeyImportExportTest (BitcoinTestFramework):
 
     def setup_chain(self):
@@ -24,7 +26,6 @@ def setup_chain(self):
 
     def setup_network(self, split=False):
         self.nodes = start_nodes(5, self.options.tmpdir, extra_args=[[
-            '-minrelaytxfee=0',
             '-allowdeprecated=getnewaddress',
             '-allowdeprecated=z_getnewaddress',
             '-allowdeprecated=z_getbalance',
@@ -39,28 +40,27 @@ def setup_network(self, split=False):
         self.sync_all()
 
     def run_test(self):
-        [alice, bob, charlie, david, miner] = self.nodes
+        [alice, bob, charlie, daira, miner] = self.nodes
+        fee = conventional_fee(2)
 
         # the sender loses 'amount' plus fee; to_addr receives exactly 'amount'
         def z_send(from_node, from_addr, to_addr, amount):
-            global fee
-            
-            opid = from_node.z_sendmany(from_addr,
-                [{"address": to_addr, "amount": Decimal(amount)}], 1, int(fee))
+            recipients = [{"address": to_addr, "amount": amount}]
+            opid = from_node.z_sendmany(from_addr, recipients, 1, fee)
             wait_and_assert_operationid_status(from_node, opid)
             self.sync_all()
             miner.generate(1)
             self.sync_all()
 
-        def verify_utxos(node, amts, zaddr):
-            amts.sort(reverse=True)
+        def verify_utxos(node, amounts, zaddr):
+            amounts.sort(reverse=True)
             txs = node.z_listreceivedbyaddress(zaddr)
             txs.sort(key=lambda x: x["amount"], reverse=True)
             print("Sorted txs", txs)
-            print("amts", amts)
+            print("amounts", amounts)
 
             try:
-                assert_equal(amts, [tx["amount"] for tx in txs])
+                assert_equal(amounts, [tx["amount"] for tx in txs])
                 for tx in txs:
                     # make sure Sapling outputs exist and have valid values
                     assert_equal("outindex" in tx, True)
@@ -68,7 +68,7 @@ def verify_utxos(node, amts, zaddr):
             except AssertionError:
                 logging.error(
                     'Expected amounts: %r; txs: %r',
-                    amts, txs)
+                    amounts, txs)
                 raise
 
         def get_private_balance(node):
@@ -91,8 +91,8 @@ def get_private_balance(node):
         # Now get a pristine z-address for receiving transfers:
         bob_zaddr = bob.z_getnewaddress()
         verify_utxos(bob, [], bob_zaddr)
-        # TODO: Verify that charlie doesn't have funds in addr
-        # verify_utxos(charlie, [])
+        assert_raises_message(JSONRPCException, "From address does not belong to this node",
+                              charlie.z_listreceivedbyaddress, bob_zaddr)
 
         # the amounts of each txn embodied which generates a single UTXO:
         amounts = list(map(Decimal, ['2.3', '3.7', '0.1', '0.5', '1.0', '0.19']))
@@ -114,7 +114,8 @@ def get_private_balance(node):
             z_send(alice, alice_zaddr, bob_zaddr, amount)
 
         verify_utxos(bob, amounts[:4], bob_zaddr)
-        # verify_utxos(charlie, [])
+        assert_raises_message(JSONRPCException, "From address does not belong to this node",
+                              charlie.z_listreceivedbyaddress, bob_zaddr)
 
         logging.info("Importing bob_privkey into charlie...")
         # z_importkey rescan defaults to "whenkeyisnew", so should rescan here
@@ -141,26 +142,24 @@ def get_private_balance(node):
         verify_utxos(charlie, amounts, ipk_zaddr["address"])
         verify_utxos(charlie, amounts, ipk_zaddr2["address"])
 
-        # keep track of the fees incurred by bob (his sends)
-        bob_fee = Decimal("0")
+        # keep track of bob's expected balance
+        bob_balance = sum(amounts)
 
         # Try to reproduce zombie balance reported in #1936
         # At generated zaddr, receive ZEC, and send ZEC back out. bob -> alice
         for amount in amounts[:2]:
             print("Sending amount from bob to alice: ", amount)
             z_send(bob, bob_zaddr, alice_zaddr, amount)
-            bob_fee += fee
+            bob_balance -= amount + fee
 
-        bob_balance = sum(amounts[2:]) - int(bob_fee)
-        
         assert_equal(bob.z_getbalance(bob_zaddr), bob_balance)
 
-        # z_import onto new node "david" (blockchain rescan, default or True?)
-        d_ipk_zaddr = david.z_importkey(bob_privkey)
+        # z_import onto new node "daira" (rescans by default because the key is new)
+        d_ipk_zaddr = daira.z_importkey(bob_privkey)
 
-        # Check if amt bob spent is deducted for charlie and david
+        # Check if amount bob spent is deducted for charlie and daira
         assert_equal(charlie.z_getbalance(ipk_zaddr["address"]), bob_balance)
-        assert_equal(david.z_getbalance(d_ipk_zaddr["address"]), bob_balance)
+        assert_equal(daira.z_getbalance(d_ipk_zaddr["address"]), bob_balance)
 
 if __name__ == '__main__':
     ZkeyImportExportTest().main()