Auto merge of #4489 - therealyingtong:4479-remove-sprout-shielding, r=str4d

[ZIP 211] Disabling Addition of New Value to the Sprout Value Pool

Disables Sprout outputs after NU4 by checking for nonzero `vpub_old` in transactions after NU4 activation height.

Adds gtests to check expected behaviour before and after NU4 activation height.

edit:
Also modifies `z_` methods in `rpcwallet`, and adds a matching RPC test.

Implements [ZIP 211](https://zips.z.cash/zip-0211), closes #4479
This commit is contained in:
Homu 2020-07-02 21:53:51 +00:00
commit 602e88ddb9
5 changed files with 236 additions and 4 deletions

View File

@ -88,6 +88,7 @@ testScripts=(
'upgrade_golden.py'
'post_heartwood_rollback.py'
'feature_logging.py'
'remove_sprout_shielding.py'
);
testScriptsExt=(
'getblocktemplate_longpoll.py'

View File

@ -0,0 +1,123 @@
#!/usr/bin/env python3
# Copyright (c) 2020 The Zcash developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or https://www.opensource.org/licenses/mit-license.php .
from decimal import Decimal
from test_framework.authproxy import JSONRPCException
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_equal,
initialize_chain,
start_nodes, get_coinbase_address,
wait_and_assert_operationid_status,
nuparams, BLOSSOM_BRANCH_ID, HEARTWOOD_BRANCH_ID, CANOPY_BRANCH_ID
)
import logging
HAS_CANOPY = ['-nurejectoldversions=false',
nuparams(BLOSSOM_BRANCH_ID, 205),
nuparams(HEARTWOOD_BRANCH_ID, 210),
nuparams(CANOPY_BRANCH_ID, 220),
]
class RemoveSproutShieldingTest (BitcoinTestFramework):
def setup_chain(self):
print("Initializing test directory "+self.options.tmpdir)
initialize_chain(self.options.tmpdir)
def setup_nodes(self):
return start_nodes(4, self.options.tmpdir, extra_args=[HAS_CANOPY]*4)
def run_test (self):
# Generate blocks up to Heartwood activation
logging.info("Generating initial blocks. Current height is 200, advance to 210 (activate Heartwood but not Canopy)")
self.nodes[0].generate(10)
self.sync_all()
# Shield coinbase to Sprout on node 0. Should pass
sprout_addr = self.nodes[0].z_getnewaddress('sprout')
myopid = self.nodes[0].z_shieldcoinbase(get_coinbase_address(self.nodes[0]), sprout_addr, 0)['opid']
wait_and_assert_operationid_status(self.nodes[0], myopid)
print("taddr -> Sprout z_shieldcoinbase tx accepted before Canopy on node 0")
self.nodes[0].generate(1)
self.sync_all()
assert_equal(self.nodes[0].z_getbalance(sprout_addr), Decimal('10'))
# Fund taddr_0 from shielded coinbase on node 0
taddr_0 = self.nodes[0].getnewaddress()
for _ in range(3):
recipients = [{"address": taddr_0, "amount": Decimal('1')}]
myopid = self.nodes[0].z_sendmany(sprout_addr, recipients, 1, 0)
wait_and_assert_operationid_status(self.nodes[0], myopid)
self.sync_all()
self.nodes[0].generate(1)
self.sync_all()
# Create taddr -> Sprout transaction and mine on node 0 before it is Canopy-aware. Should pass
sendmany_tx_0 = self.nodes[0].z_sendmany(taddr_0, [{"address": self.nodes[1].z_getnewaddress('sprout'), "amount": 1}])
wait_and_assert_operationid_status(self.nodes[0], sendmany_tx_0)
print("taddr -> Sprout z_sendmany tx accepted before Canopy on node 0")
self.nodes[0].generate(1)
self.sync_all()
# Create mergetoaddress taddr -> Sprout transaction and mine on node 0 before it is Canopy-aware. Should pass
merge_tx_0 = self.nodes[0].z_mergetoaddress(["ANY_TADDR"], self.nodes[1].z_getnewaddress('sprout'))
wait_and_assert_operationid_status(self.nodes[0], merge_tx_0['opid'])
print("taddr -> Sprout z_mergetoaddress tx accepted before Canopy on node 0")
# Mine to one block before Canopy activation on node 0; adding value
# to the Sprout pool will fail now since the transaction must be
# included in the next (or later) block, after Canopy has activated.
self.nodes[0].generate(4)
self.sync_all()
# Shield coinbase to Sprout on node 0. Should fail
errorString = ''
try:
sprout_addr = self.nodes[0].z_getnewaddress('sprout')
self.nodes[0].z_shieldcoinbase(get_coinbase_address(self.nodes[0]), sprout_addr, 0)
except JSONRPCException as e:
errorString = e.error['message']
assert("Sprout shielding is not supported after Canopy" in errorString)
print("taddr -> Sprout z_shieldcoinbase tx rejected at Canopy activation on node 0")
# Create taddr -> Sprout z_sendmany transaction on node 0. Should fail
errorString = ''
try:
sprout_addr = self.nodes[1].z_getnewaddress('sprout')
self.nodes[0].z_sendmany(taddr_0, [{"address": sprout_addr, "amount": 1}])
except JSONRPCException as e:
errorString = e.error['message']
assert("Sprout shielding is not supported after Canopy" in errorString)
print("taddr -> Sprout z_sendmany tx rejected at Canopy activation on node 0")
# Create z_mergetoaddress [taddr, Sprout] -> Sprout transaction on node 0. Should fail
errorString = ''
try:
self.nodes[0].z_mergetoaddress(["ANY_TADDR", "ANY_SPROUT"], self.nodes[1].z_getnewaddress('sprout'))
except JSONRPCException as e:
errorString = e.error['message']
assert("Sprout shielding is not supported after Canopy" in errorString)
print("[taddr, Sprout] -> Sprout z_mergetoaddress tx rejected at Canopy activation on node 0")
# Create z_mergetoaddress Sprout -> Sprout transaction on node 0. Should pass
merge_tx_1 = self.nodes[0].z_mergetoaddress(["ANY_SPROUT"], self.nodes[1].z_getnewaddress('sprout'))
wait_and_assert_operationid_status(self.nodes[0], merge_tx_1['opid'])
print("Sprout -> Sprout z_mergetoaddress tx accepted at Canopy activation on node 0")
self.nodes[0].generate(1)
self.sync_all()
# 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']
wait_and_assert_operationid_status(self.nodes[0], myopid)
print("taddr -> Sapling z_shieldcoinbase tx accepted after Canopy on node 0")
if __name__ == '__main__':
RemoveSproutShieldingTest().main()

View File

@ -1097,7 +1097,7 @@ TEST(ChecktransactionTests, BadTxReceivedOverNetwork)
}
}
TEST(CheckTransaction, InvalidShieldedCoinbase) {
TEST(ChecktransactionTests, InvalidShieldedCoinbase) {
RegtestActivateSapling();
CMutableTransaction mtx = GetValidTransaction();
@ -1128,7 +1128,7 @@ TEST(CheckTransaction, InvalidShieldedCoinbase) {
RegtestDeactivateHeartwood();
}
TEST(CheckTransaction, HeartwoodAcceptsShieldedCoinbase) {
TEST(ChecktransactionTests, HeartwoodAcceptsShieldedCoinbase) {
RegtestActivateHeartwood(false, Consensus::NetworkUpgrade::ALWAYS_ACTIVE);
auto chainparams = Params();
@ -1211,7 +1211,7 @@ TEST(CheckTransaction, HeartwoodAcceptsShieldedCoinbase) {
// Check that the consensus rules relevant to valueBalance, vShieldedOutput, and
// bindingSig from https://zips.z.cash/protocol/protocol.pdf#txnencoding are
// applied to coinbase transactions.
TEST(CheckTransaction, HeartwoodEnforcesSaplingRulesOnShieldedCoinbase) {
TEST(ChecktransactionTests, HeartwoodEnforcesSaplingRulesOnShieldedCoinbase) {
RegtestActivateHeartwood(false, Consensus::NetworkUpgrade::ALWAYS_ACTIVE);
auto chainparams = Params();
@ -1284,3 +1284,60 @@ TEST(CheckTransaction, HeartwoodEnforcesSaplingRulesOnShieldedCoinbase) {
RegtestDeactivateHeartwood();
}
TEST(ChecktransactionTests, CanopyRejectsNonzeroVPubOld) {
RegtestActivateSapling();
CMutableTransaction mtx = GetValidTransaction(NetworkUpgradeInfo[Consensus::UPGRADE_SAPLING].nBranchId);
// Make a JoinSplit with nonzero vpub_old
mtx.vJoinSplit.resize(1);
mtx.vJoinSplit[0].vpub_old = 1;
mtx.vJoinSplit[0].vpub_new = 0;
mtx.vJoinSplit[0].proof = libzcash::GrothProof();
CreateJoinSplitSignature(mtx, NetworkUpgradeInfo[Consensus::UPGRADE_SAPLING].nBranchId);
CTransaction tx(mtx);
// Before Canopy, nonzero vpub_old is accepted in both non-contextual and contextual checks
MockCValidationState state;
EXPECT_TRUE(CheckTransactionWithoutProofVerification(tx, state));
EXPECT_TRUE(ContextualCheckTransaction(tx, state, Params(), 1, true));
RegtestActivateCanopy(false, Consensus::NetworkUpgrade::ALWAYS_ACTIVE);
// After Canopy, nonzero vpub_old is accepted in non-contextual checks but rejected in contextual checks
EXPECT_TRUE(CheckTransactionWithoutProofVerification(tx, state));
EXPECT_CALL(state, DoS(100, false, REJECT_INVALID, "bad-txns-vpub_old-nonzero", false)).Times(1);
EXPECT_FALSE(ContextualCheckTransaction(tx, state, Params(), 10, true));
RegtestDeactivateCanopy();
}
TEST(ChecktransactionTests, CanopyAcceptsZeroVPubOld) {
CMutableTransaction mtx = GetValidTransaction(NetworkUpgradeInfo[Consensus::UPGRADE_SAPLING].nBranchId);
// Make a JoinSplit with zero vpub_old
mtx.vJoinSplit.resize(1);
mtx.vJoinSplit[0].vpub_old = 0;
mtx.vJoinSplit[0].vpub_new = 1;
mtx.vJoinSplit[0].proof = libzcash::GrothProof();
CreateJoinSplitSignature(mtx, NetworkUpgradeInfo[Consensus::UPGRADE_CANOPY].nBranchId);
CTransaction tx(mtx);
// After Canopy, zero value vpub_old (i.e. unshielding) is accepted in both non-contextual and contextual checks
MockCValidationState state;
RegtestActivateCanopy(false, Consensus::NetworkUpgrade::ALWAYS_ACTIVE);
EXPECT_TRUE(CheckTransactionWithoutProofVerification(tx, state));
EXPECT_TRUE(ContextualCheckTransaction(tx, state, Params(), 10, true));
RegtestDeactivateCanopy();
}

View File

@ -792,6 +792,7 @@ bool ContextualCheckTransaction(
bool saplingActive = chainparams.GetConsensus().NetworkUpgradeActive(nHeight, Consensus::UPGRADE_SAPLING);
bool isSprout = !overwinterActive;
bool heartwoodActive = chainparams.GetConsensus().NetworkUpgradeActive(nHeight, Consensus::UPGRADE_HEARTWOOD);
bool canopyActive = chainparams.GetConsensus().NetworkUpgradeActive(nHeight, Consensus::UPGRADE_CANOPY);
// If Sprout rules apply, reject transactions which are intended for Overwinter and beyond
if (isSprout && tx.fOverwintered) {
@ -936,6 +937,15 @@ bool ContextualCheckTransaction(
}
}
// Rules that apply to Canopy or later:
if (canopyActive) {
for (const JSDescription& joinsplit : tx.vJoinSplit) {
if (joinsplit.vpub_old > 0) {
return state.DoS(DOS_LEVEL_BLOCK, error("ContextualCheckTransaction(): joinsplit.vpub_old nonzero"), REJECT_INVALID, "bad-txns-vpub_old-nonzero");
}
}
}
auto consensusBranchId = CurrentEpochBranchId(nHeight, chainparams.GetConsensus());
auto prevConsensusBranchId = PrevEpochBranchId(consensusBranchId, chainparams.GetConsensus());
uint256 dataToBeSigned;

View File

@ -3017,7 +3017,14 @@ UniValue zc_raw_joinsplit(const UniValue& params, bool fHelp)
CAmount vpub_old(0);
CAmount vpub_new(0);
int nextBlockHeight = chainActive.Height() + 1;
const bool canopyActive = Params().GetConsensus().NetworkUpgradeActive(nextBlockHeight, Consensus::UPGRADE_CANOPY);
if (params[3].get_real() != 0.0)
if (canopyActive) {
throw JSONRPCError(RPC_VERIFY_REJECTED, "Sprout shielding is not supported after Canopy");
}
vpub_old = AmountFromValue(params[3]);
if (params[4].get_real() != 0.0)
@ -4063,6 +4070,15 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
RPC_INVALID_PARAMETER,
"Cannot send between Sprout and Sapling addresses using z_sendmany");
}
int nextBlockHeight = chainActive.Height() + 1;
if (fromTaddr && toSprout) {
const bool canopyActive = Params().GetConsensus().NetworkUpgradeActive(nextBlockHeight, Consensus::UPGRADE_CANOPY);
if (canopyActive) {
throw JSONRPCError(RPC_VERIFY_REJECTED, "Sprout shielding is not supported after Canopy");
}
}
} else {
throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid parameter, unknown address format: ")+address );
}
@ -4424,6 +4440,18 @@ UniValue z_shieldcoinbase(const UniValue& params, bool fHelp)
throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid parameter, unknown address format: ") + destaddress );
}
int nextBlockHeight = chainActive.Height() + 1;
const bool canopyActive = Params().GetConsensus().NetworkUpgradeActive(nextBlockHeight, Consensus::UPGRADE_CANOPY);
if (canopyActive) {
auto decodeAddr = DecodePaymentAddress(destaddress);
bool isToSproutZaddr = (boost::get<libzcash::SproutPaymentAddress>(&decodeAddr) != nullptr);
if (isToSproutZaddr) {
throw JSONRPCError(RPC_VERIFY_REJECTED, "Sprout shielding is not supported after Canopy activation");
}
}
// Convert fee from currency format to zatoshis
CAmount nFee = SHIELD_COINBASE_DEFAULT_MINERS_FEE;
if (params.size() > 2) {
@ -4442,7 +4470,6 @@ UniValue z_shieldcoinbase(const UniValue& params, bool fHelp)
}
}
int nextBlockHeight = chainActive.Height() + 1;
const bool saplingActive = Params().GetConsensus().NetworkUpgradeActive(nextBlockHeight, Consensus::UPGRADE_SAPLING);
// We cannot create shielded transactions before Sapling activates.
@ -4644,6 +4671,8 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
// Keep track of addresses to spot duplicates
std::set<std::string> setAddress;
bool isFromNonSprout = false;
// Sources
for (const UniValue& o : addresses.getValues()) {
if (!o.isStr())
@ -4653,18 +4682,24 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
if (address == "ANY_TADDR") {
useAnyUTXO = true;
isFromNonSprout = true;
} else if (address == "ANY_SPROUT") {
useAnySprout = true;
} else if (address == "ANY_SAPLING") {
useAnySapling = true;
isFromNonSprout = true;
} else {
CTxDestination taddr = DecodeDestination(address);
if (IsValidDestination(taddr)) {
taddrs.insert(taddr);
isFromNonSprout = true;
} else {
auto zaddr = DecodePaymentAddress(address);
if (IsValidPaymentAddress(zaddr)) {
zaddrs.insert(zaddr);
if (boost::get<libzcash::SaplingPaymentAddress>(&zaddr) != nullptr) {
isFromNonSprout = true;
}
} else {
throw JSONRPCError(RPC_INVALID_PARAMETER, string("Unknown address format: ") + address);
}
@ -4686,6 +4721,7 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
const int nextBlockHeight = chainActive.Height() + 1;
const bool overwinterActive = Params().GetConsensus().NetworkUpgradeActive(nextBlockHeight, Consensus::UPGRADE_OVERWINTER);
const bool saplingActive = Params().GetConsensus().NetworkUpgradeActive(nextBlockHeight, Consensus::UPGRADE_SAPLING);
const bool canopyActive = Params().GetConsensus().NetworkUpgradeActive(nextBlockHeight, Consensus::UPGRADE_CANOPY);
// Validate the destination address
auto destaddress = params[1].get_str();
@ -4709,6 +4745,11 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
}
}
if (canopyActive && isFromNonSprout && isToSproutZaddr) {
// Value can be moved within Sprout, but not into Sprout.
throw JSONRPCError(RPC_VERIFY_REJECTED, "Sprout shielding is not supported after Canopy");
}
// Convert fee from currency format to zatoshis
CAmount nFee = SHIELD_COINBASE_DEFAULT_MINERS_FEE;
if (params.size() > 2) {