#!/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()