Files
hush3/qa/rpc-tests/wallet_protectcoinbase.py
Duke Leto be16f80abc Hush Full Node is now GPLv3
Any projects which want to use Hush code from now on will need to be licensed as
GPLv3 or we will send the lawyers: https://www.softwarefreedom.org/

Notably, Komodo (KMD) is licensed as GPLv2 and is no longer compatible to receive
code changes, without causing legal issues. MIT projects, such as Zcash, also cannot pull
in changes from the Hush Full Node without permission from The Hush Developers,
which may in some circumstances grant an MIT license on a case-by-case basis.
2020-10-21 07:28:10 -04:00

365 lines
17 KiB
Python
Executable File

#!/usr/bin/env python2
# Copyright (c) 2016 The Zcash developers
# Distributed under the GPLv3 software license, see the accompanying
# file COPYING or https://www.gnu.org/licenses/gpl-3.0.en.html
from test_framework.test_framework import BitcoinTestFramework
from test_framework.authproxy import JSONRPCException
from test_framework.mininode import COIN
from test_framework.util import assert_equal, initialize_chain_clean, \
start_nodes, connect_nodes_bi, wait_and_assert_operationid_status
import sys
import timeit
from decimal import Decimal
def check_value_pool(node, name, total):
value_pools = node.getblockchaininfo()['valuePools']
found = False
for pool in value_pools:
if pool['id'] == name:
found = True
assert_equal(pool['monitored'], True)
assert_equal(pool['chainValue'], total)
assert_equal(pool['chainValueZat'], total * COIN)
assert(found)
class WalletProtectCoinbaseTest (BitcoinTestFramework):
def setup_chain(self):
print("Initializing test directory "+self.options.tmpdir)
initialize_chain_clean(self.options.tmpdir, 4)
# Start nodes with -regtestprotectcoinbase to set fCoinbaseMustBeProtected to true.
def setup_network(self, split=False):
self.nodes = start_nodes(4, self.options.tmpdir, extra_args=[['-regtestprotectcoinbase', '-debug=zrpcunsafe']] * 4 )
connect_nodes_bi(self.nodes,0,1)
connect_nodes_bi(self.nodes,1,2)
connect_nodes_bi(self.nodes,0,2)
connect_nodes_bi(self.nodes,0,3)
self.is_network_split=False
self.sync_all()
def run_test (self):
print "Mining blocks..."
self.nodes[0].generate(4)
walletinfo = self.nodes[0].getwalletinfo()
assert_equal(walletinfo['immature_balance'], 40)
assert_equal(walletinfo['balance'], 0)
self.sync_all()
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[3].getbalance(), 0)
check_value_pool(self.nodes[0], 'sprout', 0)
check_value_pool(self.nodes[1], 'sprout', 0)
check_value_pool(self.nodes[2], 'sprout', 0)
check_value_pool(self.nodes[3], 'sprout', 0)
# Send will fail because we are enforcing the consensus rule that
# coinbase utxos can only be sent to a zaddr.
errorString = ""
try:
self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 1)
except JSONRPCException,e:
errorString = e.error['message']
assert_equal("Coinbase funds can only be sent to a zaddr" in errorString, True)
# Prepare to send taddr->zaddr
mytaddr = self.nodes[0].getnewaddress()
myzaddr = self.nodes[0].z_getnewaddress()
# Node 3 will test that watch only address utxos are not selected
self.nodes[3].importaddress(mytaddr)
recipients= [{"address":myzaddr, "amount": Decimal('1')}]
myopid = self.nodes[3].z_sendmany(mytaddr, recipients)
wait_and_assert_operationid_status(self.nodes[3], myopid, "failed", "no UTXOs found for taddr from address", 10)
# This send will fail because our wallet does not allow any change when protecting a coinbase utxo,
# as it's currently not possible to specify a change address in z_sendmany.
recipients = []
recipients.append({"address":myzaddr, "amount":Decimal('1.23456789')})
myopid = self.nodes[0].z_sendmany(mytaddr, recipients)
error_result = wait_and_assert_operationid_status(self.nodes[0], myopid, "failed", "wallet does not allow any 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"], Decimal('0.0001')) # default
assert_equal(params["minconf"], Decimal('1')) # default
assert_equal(params["fromaddress"], mytaddr)
assert_equal(params["amounts"][0]["address"], myzaddr)
assert_equal(params["amounts"][0]["amount"], Decimal('1.23456789'))
# Add viewing key for myzaddr to Node 3
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 fee of 0.00010000, with no change.
shieldvalue = Decimal('20.0') - Decimal('0.0001')
recipients = []
recipients.append({"address":myzaddr, "amount": shieldvalue})
myopid = self.nodes[0].z_sendmany(mytaddr, recipients)
mytxid = wait_and_assert_operationid_status(self.nodes[0], myopid)
self.sync_all()
# Verify that z_listunspent can return a note that has zero confirmations
results = self.nodes[0].z_listunspent()
assert(len(results) == 0)
results = self.nodes[0].z_listunspent(0) # set minconf to zero
assert(len(results) == 1)
assert_equal(results[0]["address"], myzaddr)
assert_equal(results[0]["amount"], shieldvalue)
assert_equal(results[0]["confirmations"], 0)
# Mine the tx
self.nodes[1].generate(1)
self.sync_all()
# Verify that z_listunspent returns one note which has been confirmed
results = self.nodes[0].z_listunspent()
assert(len(results) == 1)
assert_equal(results[0]["address"], myzaddr)
assert_equal(results[0]["amount"], shieldvalue)
assert_equal(results[0]["confirmations"], 1)
assert_equal(results[0]["spendable"], True)
# Verify that z_listunspent returns note for watchonly address on node 3.
results = self.nodes[3].z_listunspent(1, 999, True)
assert(len(results) == 1)
assert_equal(results[0]["address"], myzaddr)
assert_equal(results[0]["amount"], shieldvalue)
assert_equal(results[0]["confirmations"], 1)
assert_equal(results[0]["spendable"], False)
# Verify that z_listunspent returns error when address spending key from node 0 is not available in wallet of node 1.
try:
results = self.nodes[1].z_listunspent(1, 999, False, [myzaddr])
except JSONRPCException as e:
errorString = e.error['message']
assert_equal("Invalid parameter, spending key for address does not belong to wallet" in errorString, True)
# Verify that debug=zrpcunsafe logs params, and that full txid is associated with opid
logpath = self.options.tmpdir+"/node0/regtest/debug.log"
logcounter = 0
with open(logpath, "r") as myfile:
logdata = myfile.readlines()
for logline in logdata:
if myopid + ": z_sendmany initialized" in logline and mytaddr in logline and myzaddr in logline:
assert_equal(logcounter, 0) # verify order of log messages
logcounter = logcounter + 1
if myopid + ": z_sendmany finished" in logline and mytxid in logline:
assert_equal(logcounter, 1)
logcounter = logcounter + 1
assert_equal(logcounter, 2)
# check balances (the z_sendmany consumes 3 coinbase utxos)
resp = self.nodes[0].z_gettotalbalance()
assert_equal(Decimal(resp["transparent"]), Decimal('20.0'))
assert_equal(Decimal(resp["private"]), Decimal('19.9999'))
assert_equal(Decimal(resp["total"]), Decimal('39.9999'))
# The Sprout value pool should reflect the send
sproutvalue = shieldvalue
check_value_pool(self.nodes[0], 'sprout', sproutvalue)
# A custom fee of 0 is okay. Here the node will send the note value back to itself.
recipients = []
recipients.append({"address":myzaddr, "amount": Decimal('19.9999')})
myopid = self.nodes[0].z_sendmany(myzaddr, recipients, 1, Decimal('0.0'))
mytxid = wait_and_assert_operationid_status(self.nodes[0], myopid)
self.sync_all()
self.nodes[1].generate(1)
self.sync_all()
resp = self.nodes[0].z_gettotalbalance()
assert_equal(Decimal(resp["transparent"]), Decimal('20.0'))
assert_equal(Decimal(resp["private"]), Decimal('19.9999'))
assert_equal(Decimal(resp["total"]), Decimal('39.9999'))
# The Sprout value pool should be unchanged
check_value_pool(self.nodes[0], 'sprout', sproutvalue)
# convert note to transparent funds
unshieldvalue = Decimal('10.0')
recipients = []
recipients.append({"address":mytaddr, "amount": unshieldvalue})
myopid = self.nodes[0].z_sendmany(myzaddr, recipients)
mytxid = wait_and_assert_operationid_status(self.nodes[0], myopid)
assert(mytxid is not None)
self.sync_all()
# check that priority of the tx sending from a zaddr is not 0
mempool = self.nodes[0].getrawmempool(True)
assert(Decimal(mempool[mytxid]['startingpriority']) >= Decimal('1000000000000'))
self.nodes[1].generate(1)
self.sync_all()
# check balances
sproutvalue -= unshieldvalue + Decimal('0.0001')
resp = self.nodes[0].z_gettotalbalance()
assert_equal(Decimal(resp["transparent"]), Decimal('30.0'))
assert_equal(Decimal(resp["private"]), Decimal('9.9998'))
assert_equal(Decimal(resp["total"]), Decimal('39.9998'))
check_value_pool(self.nodes[0], 'sprout', sproutvalue)
# 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 and standard z_sendmany fee is 0.0001.
recipients = []
amount = Decimal('10.0') - Decimal('0.00010000') - 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)
wait_and_assert_operationid_status(self.nodes[0], myopid, "failed", "Insufficient transparent funds, have 10.00, need 0.00000053 more to avoid creating invalid change output 0.00000001 (dust threshold is 0.00000054)")
# Send will fail because send amount is too big, even when including coinbase utxos
errorString = ""
try:
self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 99999)
except JSONRPCException,e:
errorString = e.error['message']
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)
wait_and_assert_operationid_status(self.nodes[0], myopid, "failed", "Insufficient transparent funds, have 10.00, need 10000.0001")
myopid = self.nodes[0].z_sendmany(myzaddr, recipients)
wait_and_assert_operationid_status(self.nodes[0], myopid, "failed", "Insufficient shielded funds, have 9.9998, need 10000.0001")
# Send will fail because of insufficient funds unless sender uses coinbase utxos
try:
self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 21)
except JSONRPCException,e:
errorString = e.error['message']
assert_equal("Insufficient funds, coinbase funds can only be spent after they have been sent to a zaddr" in errorString, True)
# Verify that mempools accept tx with joinsplits which have at least the default z_sendmany fee.
# If this test passes, it confirms that issue #1851 has been resolved, where sending from
# a zaddr to 1385 taddr recipients fails because the default fee was considered too low
# given the tx size, resulting in mempool rejection.
errorString = ''
recipients = []
num_t_recipients = 2500
amount_per_recipient = Decimal('0.00000546') # dust threshold
# Note that regtest chainparams does not require standard tx, so setting the amount to be
# less than the dust threshold, e.g. 0.00000001 will not result in mempool rejection.
start_time = timeit.default_timer()
for i in xrange(0,num_t_recipients):
newtaddr = self.nodes[2].getnewaddress()
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))
# Issue #2263 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 #2263 Workaround END
myopid = self.nodes[0].z_sendmany(myzaddr, recipients)
try:
wait_and_assert_operationid_status(self.nodes[0], myopid)
except JSONRPCException as e:
print("JSONRPC error: "+e.error['message'])
assert(False)
except Exception as e:
print("Unexpected exception caught during testing: "+str(sys.exc_info()[0]))
assert(False)
self.sync_all()
self.nodes[1].generate(1)
self.sync_all()
# check balance
node2balance = amount_per_recipient * num_t_recipients
sproutvalue -= node2balance + Decimal('0.0001')
assert_equal(self.nodes[2].getbalance(), node2balance)
check_value_pool(self.nodes[0], 'sprout', sproutvalue)
# Send will fail because fee is negative
try:
self.nodes[0].z_sendmany(myzaddr, recipients, 1, -1)
except JSONRPCException,e:
errorString = e.error['message']
assert_equal("Amount out of range" in errorString, True)
# Send will fail because fee is larger than MAX_MONEY
try:
self.nodes[0].z_sendmany(myzaddr, recipients, 1, Decimal('21000000.00000001'))
except JSONRPCException,e:
errorString = e.error['message']
assert_equal("Amount out of range" in errorString, True)
# Send will fail because fee is larger than sum of outputs
try:
self.nodes[0].z_sendmany(myzaddr, recipients, 1, (amount_per_recipient * num_t_recipients) + Decimal('0.00000001'))
except JSONRPCException,e:
errorString = e.error['message']
assert_equal("is greater than the sum of outputs" in errorString, True)
# Send will succeed because the balance of non-coinbase utxos is 10.0
try:
self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 9)
except JSONRPCException:
assert(False)
self.sync_all()
self.nodes[1].generate(1)
self.sync_all()
# check balance
node2balance = node2balance + 9
assert_equal(self.nodes[2].getbalance(), node2balance)
# Check that chained joinsplits in a single tx are created successfully.
recipients = []
num_recipients = 3
amount_per_recipient = Decimal('0.002')
minconf = 1
send_amount = num_recipients * amount_per_recipient
custom_fee = Decimal('0.00012345')
zbalance = self.nodes[0].z_getbalance(myzaddr)
for i in xrange(0,num_recipients):
newzaddr = self.nodes[2].z_getnewaddress()
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()
self.nodes[1].generate(1)
self.sync_all()
# check balances and unspent notes
resp = self.nodes[2].z_gettotalbalance()
assert_equal(Decimal(resp["private"]), send_amount)
notes = self.nodes[2].z_listunspent()
sum_of_notes = sum([note["amount"] for note in notes])
assert_equal(Decimal(resp["private"]), sum_of_notes)
resp = self.nodes[0].z_getbalance(myzaddr)
assert_equal(Decimal(resp), zbalance - custom_fee - send_amount)
sproutvalue -= custom_fee
check_value_pool(self.nodes[0], 'sprout', sproutvalue)
notes = self.nodes[0].z_listunspent(1, 99999, False, [myzaddr])
sum_of_notes = sum([note["amount"] for note in notes])
assert_equal(Decimal(resp), sum_of_notes)
if __name__ == '__main__':
WalletProtectCoinbaseTest().main()