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.
365 lines
17 KiB
Python
Executable File
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()
|