From 4c1a8884f41d83f9af67447fa0e749b7164a431c Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 19 Apr 2019 10:54:51 -0700 Subject: [PATCH 1/2] Add testnet and regtest experimental feature: -developersetpoolsizezero --- src/chainparams.cpp | 9 +++++++++ src/init.cpp | 5 +++-- src/main.cpp | 19 +++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/chainparams.cpp b/src/chainparams.cpp index 45b77a09e..5857b0776 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -491,6 +491,10 @@ public: assert(idx > Consensus::BASE_SPROUT && idx < Consensus::MAX_NETWORK_UPGRADES); consensus.vUpgrades[idx].nActivationHeight = nActivationHeight; } + + void SetRegTestZIP209Enabled() { + fZIP209Enabled = true; + } }; static CRegTestParams regTestParams; @@ -523,6 +527,11 @@ void SelectParams(CBaseChainParams::Network network) { if (network == CBaseChainParams::REGTEST && mapArgs.count("-regtestprotectcoinbase")) { regTestParams.SetRegTestCoinbaseMustBeProtected(); } + + // When a developer is debugging turnstile violations in regtest mode, enable ZIP209 + if (network == CBaseChainParams::REGTEST && mapArgs.count("-developersetpoolsizezero")) { + regTestParams.SetRegTestZIP209Enabled(); + } } bool SelectParamsFromCommandLine() diff --git a/src/init.cpp b/src/init.cpp index ec459e36b..d52e1baa3 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -834,8 +834,9 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler) if (!fExperimentalMode) { if (mapArgs.count("-developerencryptwallet")) { return InitError(_("Wallet encryption requires -experimentalfeatures.")); - } - else if (mapArgs.count("-paymentdisclosure")) { + } else if (mapArgs.count("-developersetpoolsizezero")) { + return InitError(_("Setting the size of shielded pools to zero requires -experimentalfeatures.")); + } else if (mapArgs.count("-paymentdisclosure")) { return InitError(_("Payment disclosure requires -experimentalfeatures.")); } else if (mapArgs.count("-zmergetoaddress")) { return InitError(_("RPC method z_mergetoaddress requires -experimentalfeatures.")); diff --git a/src/main.cpp b/src/main.cpp index 4c47cfa0c..72744b61a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3457,6 +3457,14 @@ void FallbackSproutValuePoolBalance( return; } + // When developer option -developersetpoolsizezero is enabled, we don't need a fallback balance. + if ((chainparams.NetworkIDString() == "test" || chainparams.NetworkIDString() == "regtest") && + fExperimentalMode && + mapArgs.count("-developersetpoolsizezero")) + { + return; + } + // Check if the height of this block matches the checkpoint if (pindex->nHeight == chainparams.SproutValuePoolCheckpointHeight()) { if (pindex->GetBlockHash() == chainparams.SproutValuePoolCheckpointBlockHash()) { @@ -4243,6 +4251,17 @@ bool static LoadBlockIndexDB() // Fall back to hardcoded Sprout value pool balance FallbackSproutValuePoolBalance(pindex, chainparams); + + // If developer option -developersetpoolsizezero has been enabled on testnet or in regtest mode, + // override and set the in-memory size of shielded pools to zero. An unshielding transaction + // can then be used to trigger and test the handling of turnstile violations. + if ((chainparams.NetworkIDString() == "test" || chainparams.NetworkIDString() == "regtest") && + fExperimentalMode && + mapArgs.count("-developersetpoolsizezero")) + { + pindex->nChainSproutValue = 0; + pindex->nChainSaplingValue = 0; + } } // Construct in-memory chain of branch IDs. // Relies on invariant: a block that does not activate a network upgrade From d6b47948bf04800e603708d034eb9dc9883e24e7 Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 19 Apr 2019 10:57:12 -0700 Subject: [PATCH 2/2] Add qa test for experimental feature: -developersetpoolsizezero --- qa/pull-tester/rpc-tests.sh | 1 + qa/rpc-tests/turnstile.py | 204 ++++++++++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+) create mode 100755 qa/rpc-tests/turnstile.py diff --git a/qa/pull-tester/rpc-tests.sh b/qa/pull-tester/rpc-tests.sh index 3d6be26dd..19c65a0b5 100755 --- a/qa/pull-tester/rpc-tests.sh +++ b/qa/pull-tester/rpc-tests.sh @@ -71,6 +71,7 @@ testScripts=( 'p2p_node_bloom.py' 'regtest_signrawtransaction.py' 'finalsaplingroot.py' + 'turnstile.py' ); testScriptsExt=( 'getblocktemplate_longpoll.py' diff --git a/qa/rpc-tests/turnstile.py b/qa/rpc-tests/turnstile.py new file mode 100755 index 000000000..89d680f0b --- /dev/null +++ b/qa/rpc-tests/turnstile.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python +# Copyright (c) 2019 The Zcash developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +# +# Test Sprout and Sapling turnstile violations +# +# Experimental feature -developersetpoolsizezero will, upon node launch, +# set the in-memory size of shielded pools to zero. +# +# An unshielding operation can then be used to verify: +# 1. Turnstile violating transactions are excluded by the miner +# 2. Turnstile violating blocks are rejected by nodes +# +# By default, ZIP209 support is disabled in regtest mode, but gets enabled +# when experimental feature -developersetpoolsizezero is switched on. +# +# To perform a manual turnstile test on testnet: +# 1. Launch zcashd +# 2. Shield transparent funds +# 3. Wait for transaction to be mined +# 4. Restart zcashd, enabling experimental feature -developersetpoolsizezero +# 5. Unshield funds +# 6. Wait for transaction to be mined (using testnet explorer or another node) +# 7. Verify zcashd rejected the block +# + +import sys; assert sys.version_info < (3,), ur"This script does not run under Python 3. Please use Python 2.7.x." + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, + get_coinbase_address, + start_node, start_nodes, + sync_blocks, sync_mempools, + initialize_chain_clean, connect_nodes_bi, + wait_and_assert_operationid_status, + bitcoind_processes +) +from decimal import Decimal + +NUPARAMS_ARGS = ['-nuparams=5ba81b19:100', # Overwinter + '-nuparams=76b809bb:101'] # Sapling +TURNSTILE_ARGS = ['-experimentalfeatures', + '-developersetpoolsizezero'] + +class TurnstileTest (BitcoinTestFramework): + + def setup_chain(self): + print("Initializing test directory " + self.options.tmpdir) + initialize_chain_clean(self.options.tmpdir, 3) + + def setup_network(self, split=False): + self.nodes = start_nodes(3, self.options.tmpdir, + extra_args=[NUPARAMS_ARGS] * 3) + connect_nodes_bi(self.nodes,0,1) + connect_nodes_bi(self.nodes,1,2) + self.is_network_split=False + self.sync_all() + + # Helper method to verify the size of a shielded value pool for a given node + def assert_pool_balance(self, node, name, balance): + pools = node.getblockchaininfo()['valuePools'] + for pool in pools: + if pool['id'] == name: + assert_equal(pool['chainValue'], balance, message="for pool named %r" % (name,)) + return + assert False, "pool named %r not found" % (name,) + + # Helper method to start a single node with extra args and sync to the network + def start_and_sync_node(self, index, args=[]): + self.nodes[index] = start_node(index, self.options.tmpdir, extra_args=NUPARAMS_ARGS + args) + connect_nodes_bi(self.nodes,0,1) + connect_nodes_bi(self.nodes,1,2) + connect_nodes_bi(self.nodes,0,2) + self.sync_all() + + # Helper method to stop and restart a single node with extra args and sync to the network + def restart_and_sync_node(self, index, args=[]): + self.nodes[index].stop() + bitcoind_processes[index].wait() + self.start_and_sync_node(index, args) + + def run_test(self): + # Sanity-check the test harness + self.nodes[0].generate(101) + assert_equal(self.nodes[0].getblockcount(), 101) + self.sync_all() + + # Node 0 shields some funds + dest_addr = self.nodes[0].z_getnewaddress(POOL_NAME.lower()) + taddr0 = get_coinbase_address(self.nodes[0]) + recipients = [] + recipients.append({"address": dest_addr, "amount": Decimal('10')}) + myopid = self.nodes[0].z_sendmany(taddr0, recipients, 1, 0) + wait_and_assert_operationid_status(self.nodes[0], myopid) + self.sync_all() + self.nodes[0].generate(1) + self.sync_all() + assert_equal(self.nodes[0].z_getbalance(dest_addr), Decimal('10')) + + # Verify size of shielded pool + self.assert_pool_balance(self.nodes[0], POOL_NAME.lower(), Decimal('10')) + self.assert_pool_balance(self.nodes[1], POOL_NAME.lower(), Decimal('10')) + self.assert_pool_balance(self.nodes[2], POOL_NAME.lower(), Decimal('10')) + + # Relaunch node 0 with in-memory size of value pools set to zero. + self.restart_and_sync_node(0, TURNSTILE_ARGS) + + # 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('10')) + self.assert_pool_balance(self.nodes[2], POOL_NAME.lower(), Decimal('10')) + + # 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) + mytxid = wait_and_assert_operationid_status(self.nodes[0], myopid) + + # Verify transaction appears in mempool of nodes + self.sync_all() + assert(mytxid in self.nodes[0].getrawmempool()) + assert(mytxid in self.nodes[1].getrawmempool()) + assert(mytxid in self.nodes[2].getrawmempool()) + + # Node 0 mines a block + count = self.nodes[0].getblockcount() + self.nodes[0].generate(1) + self.sync_all() + + # Verify the mined block does not contain the unshielding transaction + block = self.nodes[0].getblock(self.nodes[0].getbestblockhash()) + assert_equal(len(block["tx"]), 1) + assert_equal(block["height"], count + 1) + + # Stop node 0 and check logs to verify the miner excluded the transaction from the block + self.nodes[0].stop() + bitcoind_processes[0].wait() + logpath = self.options.tmpdir + "/node0/regtest/debug.log" + foundErrorMsg = False + with open(logpath, "r") as myfile: + logdata = myfile.readlines() + for logline in logdata: + if "CreateNewBlock(): tx " + mytxid + " appears to violate " + POOL_NAME.capitalize() + " turnstile" in logline: + foundErrorMsg = True + break + assert(foundErrorMsg) + + # Launch node 0 with in-memory size of value pools set to zero. + self.start_and_sync_node(0, TURNSTILE_ARGS) + + # Node 1 mines a block + oldhash = self.nodes[0].getbestblockhash() + self.nodes[1].generate(1) + newhash = self.nodes[1].getbestblockhash() + + # Verify block contains the unshielding transaction + assert(mytxid in self.nodes[1].getblock(newhash)["tx"]) + + # Verify nodes 1 and 2 have accepted the block as valid + sync_blocks(self.nodes[1:3]) + sync_mempools(self.nodes[1:3]) + assert_equal(len(self.nodes[1].getrawmempool()), 0) + assert_equal(len(self.nodes[2].getrawmempool()), 0) + + # Verify node 0 has not accepted the block + assert_equal(oldhash, self.nodes[0].getbestblockhash()) + assert(mytxid in self.nodes[0].getrawmempool()) + self.assert_pool_balance(self.nodes[0], POOL_NAME.lower(), Decimal('0')) + + # 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('9')) + self.assert_pool_balance(self.nodes[2], POOL_NAME.lower(), Decimal('9')) + + # Stop node 0 and check logs to verify the block was rejected as a turnstile violation + self.nodes[0].stop() + bitcoind_processes[0].wait() + logpath = self.options.tmpdir + "/node0/regtest/debug.log" + foundConnectBlockErrorMsg = False + foundInvalidBlockErrorMsg = False + foundConnectTipErrorMsg = False + with open(logpath, "r") as myfile: + logdata = myfile.readlines() + for logline in logdata: + if "ConnectBlock(): turnstile violation in " + POOL_NAME.capitalize() + " shielded value pool" in logline: + foundConnectBlockErrorMsg = True + elif "InvalidChainFound: invalid block=" + newhash in logline: + foundInvalidBlockErrorMsg = True + elif "ConnectTip(): ConnectBlock " + newhash + " failed" in logline: + foundConnectTipErrorMsg = True + assert(foundConnectBlockErrorMsg and foundInvalidBlockErrorMsg and foundConnectTipErrorMsg) + + # Launch node 0 without overriding the pool size, so the node can sync with rest of network. + self.start_and_sync_node(0) + assert_equal(newhash, self.nodes[0].getbestblockhash()) + +if __name__ == '__main__': + POOL_NAME = "SPROUT" + TurnstileTest().main() + POOL_NAME = "SAPLING" + TurnstileTest().main()