Files
dragonx/qa/rpc-tests/wallet_sapling.py
DanS c5fde12485 qa: port 12 wallet/mining tests to python3 and fix three framework blockers
Ports qa/rpc-tests from 6 python3 files to 18. Ran every ported test against
the freshly built v1.3.0 dragonxd. Results, honestly:

  PASS (1)            getblocktemplate_proposals.py
  NOT APPLICABLE (7)  wallet.py, walletbackup.py, wallet_protectcoinbase.py,
                      wallet_listnotes.py, wallet_mergetoaddress.py,
                      getblocktemplate.py, wallet_shieldcoinbase.py
  BLOCKED (4)         wallet_sapling, wallet_nullifiers, wallet_persistence,
                      wallet_treestate

The "not applicable" seven are inherited Zcash/Hush-era tests that exercise
features DragonX deliberately removed. They assume transparent t->t value
transfer, but ASSETCHAINS_PRIVATE=1 (hush_utils.h:1826) makes sendtoaddress
and sendmany consensus-refuse; they assume Sprout joinsplits, which are gone
from the RPC layer entirely; and they hardcode Bitcoin economics (10 coin/block,
100-block maturity) against DragonX's 3 DRGX and COINBASE_MATURITY=1. They are
ported and left in place rather than deleted, but they cannot pass on this chain
without being rewritten around z_shieldcoinbase/autoshield.

Three framework fixes in test_framework/util.py, each of which broke every
multi-node test:
  - initialize_chain() passed -connect=0, and init.cpp soft-sets -listen=0 when
    -connect is present, so cache node0 never opened its p2p port and nodes 1-3
    could never sync to it -- initialize_chain() hung forever in sync_blocks().
    Now passes -listen=1 -bind=127.0.0.1 -dnsseed=0 explicitly (an explicit arg
    beats SoftSetBoolArg) while keeping the cache nodes off the public network.
  - cache cleanup removed files from <datadir> when dragonxd writes them one
    level deeper into the net-specific <datadir>/regtest.
  - set_node_times() did print("..." + t) with t an int -> TypeError.
  - default binary paths corrected to src/dragonxd and src/dragonx-cli.

The four BLOCKED tests are blocked by a daemon assert, not by the port; see the
follow-up commit/report on BLOCK_VALID_CONTEXT.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FU87LdsJZiZkfq1eXubpeo
2026-08-31 01:53:43 -05:00

233 lines
10 KiB
Python
Executable File

#!/usr/bin/env python3
# Copyright (c) 2016-2024 The Hush developers
# Copyright (c) 2018 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.util import (
assert_equal,
initialize_chain_clean,
p2p_port,
set_node_times,
start_node,
sync_blocks,
wait_and_assert_operationid_status,
)
import os
import stat
from decimal import Decimal
# Test wallet behaviour with Sapling addresses
class WalletSaplingTest(BitcoinTestFramework):
def setup_chain(self):
# The shared initialize_chain() cache builder in test_framework/util.py has not been
# repaired for DragonX (it spawns the cache nodes by bare name off PATH, drives the
# CLI without -regtest so it dials the assetchain RPC port, and deletes debug.log
# from the non-net-specific datadir). Build the same starting state here instead --
# see _generate_starting_chain() -- so this test does not depend on it.
print("Initializing test directory " + self.options.tmpdir)
initialize_chain_clean(self.options.tmpdir, 4)
# !!! test_framework/util.py:start_node() hardcodes "-connect=0" into every regtest
# node's argv. Modern Bitcoin Core special-cases that value to mean "make no automatic
# connections", but THIS codebase does not (net.cpp ThreadOpenConnections just iterates
# mapMultiArgs["-connect"]), so "0" is dialled as a hostname: it resolves to 0.0.0.0,
# which on Linux connects to localhost on Params().GetDefaultPort() -- 21768, the live
# DRAGONX p2p port. Observed directly: a regtest node started by the unmodified
# framework peered with the production dragonxd on this host and with seven public
# mainnet nodes (heights ~3.25M) and began ingesting mainnet headers. "-connect" also
# soft-sets "-listen=0", so the framework's own connect_nodes_bi() can never establish
# the local links a multi-node test needs.
#
# Both problems are in the shared framework, which this port is not allowed to touch, so
# they are worked around per-node here: the daemon is launched through a tiny wrapper
# that strips the "-connect=0" argument, and the real topology/listening flags are passed
# as extra_args (which start_node appends after its own).
def _daemon_wrapper(self):
srcdir = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "..", "..", "src", "dragonxd")
path = os.path.join(self.options.tmpdir, "dragonxd-no-connect0")
with open(path, "w") as f:
f.write("#!/usr/bin/env bash\n")
f.write("args=()\n")
f.write('for a in "$@"; do\n')
f.write(' if [ "$a" = "-connect=0" ]; then continue; fi\n')
f.write(' args+=("$a")\n')
f.write("done\n")
f.write('exec %s "${args[@]}"\n' % os.path.realpath(srcdir))
os.chmod(path, os.stat(path).st_mode | stat.S_IXUSR)
return path
def setup_nodes(self):
binary = self._daemon_wrapper()
nodes = []
for i in range(4):
extra_args = [
#'-nuparams=5ba81b19:201', # Overwinter
#'-nuparams=76b809bb:203', # Sapling
#'-experimentalfeatures', '-zmergetoaddress',
# Listen on this test's PID-keyed port so the nodes can actually peer with
# each other, and only ever dial each other -- never the public network.
'-listen=1',
'-bind=127.0.0.1',
'-port=%d' % p2p_port(i),
] + ['-connect=127.0.0.1:%d' % p2p_port(j) for j in range(4) if j != i]
nodes.append(start_node(i, self.options.tmpdir, extra_args, binary=binary))
return nodes
def setup_network(self, split=False):
super(WalletSaplingTest, self).setup_network(split)
self._generate_starting_chain()
def _generate_starting_chain(self):
# Equivalent of test_framework.util.initialize_chain(): a 200-block chain where each
# of the 4 nodes mined 25 blocks twice, so every node holds 25 mature and 25 immature
# coinbases. Block timestamps are 10 minutes apart starting 1 Jan 2014, as there.
block_time = 1388534400
for _round in range(2):
for peer in range(4):
for _j in range(25):
set_node_times(self.nodes, block_time)
self.nodes[peer].generate(1)
block_time += 10 * 75
# Must sync before next peer starts generating blocks
sync_blocks(self.nodes)
# Drop back to wall-clock time: initialize_chain() stops the cache nodes and the test
# then runs against freshly started nodes that have no mocktime set.
set_node_times(self.nodes, 0)
self.sync_all()
def run_test(self):
# Sanity-check the test harness
assert_equal(self.nodes[0].getblockcount(), 200)
# Activate Overwinter
self.nodes[2].generate(1)
self.sync_all()
self.nodes[2].generate(2)
self.sync_all()
taddr0 = self.nodes[0].getnewaddress()
# Skip over the address containing node 1's coinbase
self.nodes[1].getnewaddress()
taddr1 = self.nodes[1].getnewaddress()
saplingAddr0 = self.nodes[0].z_getnewaddress('sapling')
saplingAddr1 = self.nodes[1].z_getnewaddress('sapling')
# Verify addresses
assert(saplingAddr0 in self.nodes[0].z_listaddresses())
assert(saplingAddr1 in self.nodes[1].z_listaddresses())
assert_equal(self.nodes[0].z_validateaddress(saplingAddr0)['type'], 'sapling')
assert_equal(self.nodes[0].z_validateaddress(saplingAddr1)['type'], 'sapling')
# Verify balance
assert_equal(self.nodes[0].z_getbalance(saplingAddr0), Decimal('0'))
assert_equal(self.nodes[1].z_getbalance(saplingAddr1), Decimal('0'))
assert_equal(self.nodes[1].z_getbalance(taddr1), Decimal('0'))
# Node 0 shields some funds
# taddr -> Sapling
# -> taddr (change)
recipients = []
recipients.append({"address": saplingAddr0, "amount": Decimal('20')})
myopid = self.nodes[0].z_sendmany(taddr0, recipients, 1, 0)
mytxid = wait_and_assert_operationid_status(self.nodes[0], myopid)
self.sync_all()
# Verify priority of tx is MAX_PRIORITY, defined as 1E+16 (10000000000000000)
mempool = self.nodes[0].getrawmempool(True)
assert(Decimal(mempool[mytxid]['startingpriority']) == Decimal('1E+16'))
self.nodes[2].generate(1)
self.sync_all()
# Verify balance
assert_equal(self.nodes[0].z_getbalance(saplingAddr0), Decimal('20'))
assert_equal(self.nodes[1].z_getbalance(saplingAddr1), Decimal('0'))
assert_equal(self.nodes[1].z_getbalance(taddr1), Decimal('0'))
# Node 0 sends some shielded funds to node 1
# Sapling -> Sapling
# -> Sapling (change)
recipients = []
recipients.append({"address": saplingAddr1, "amount": Decimal('15')})
myopid = self.nodes[0].z_sendmany(saplingAddr0, recipients, 1, 0)
mytxid = wait_and_assert_operationid_status(self.nodes[0], myopid)
self.sync_all()
# Verify priority of tx is MAX_PRIORITY, defined as 1E+16 (10000000000000000)
mempool = self.nodes[0].getrawmempool(True)
assert(Decimal(mempool[mytxid]['startingpriority']) == Decimal('1E+16'))
self.nodes[2].generate(1)
self.sync_all()
# Verify balance
assert_equal(self.nodes[0].z_getbalance(saplingAddr0), Decimal('5'))
assert_equal(self.nodes[1].z_getbalance(saplingAddr1), Decimal('15'))
assert_equal(self.nodes[1].z_getbalance(taddr1), Decimal('0'))
# Node 1 sends some shielded funds to node 0, as well as unshielding
# Sapling -> Sapling
# -> taddr
# -> Sapling (change)
recipients = []
recipients.append({"address": saplingAddr0, "amount": Decimal('5')})
recipients.append({"address": taddr1, "amount": Decimal('5')})
myopid = self.nodes[1].z_sendmany(saplingAddr1, recipients, 1, 0)
mytxid = wait_and_assert_operationid_status(self.nodes[1], myopid)
self.sync_all()
# Verify priority of tx is MAX_PRIORITY, defined as 1E+16 (10000000000000000)
mempool = self.nodes[1].getrawmempool(True)
assert(Decimal(mempool[mytxid]['startingpriority']) == Decimal('1E+16'))
self.nodes[2].generate(1)
self.sync_all()
# Verify balance
assert_equal(self.nodes[0].z_getbalance(saplingAddr0), Decimal('10'))
assert_equal(self.nodes[1].z_getbalance(saplingAddr1), Decimal('5'))
assert_equal(self.nodes[1].z_getbalance(taddr1), Decimal('5'))
# Verify existence of Sapling related JSON fields
resp = self.nodes[0].getrawtransaction(mytxid, 1)
assert_equal(resp['valueBalance'], Decimal('5'))
assert(len(resp['vShieldedSpend']) == 1)
assert(len(resp['vShieldedOutput']) == 2)
assert('bindingSig' in resp)
shieldedSpend = resp['vShieldedSpend'][0]
assert('cv' in shieldedSpend)
assert('anchor' in shieldedSpend)
assert('nullifier' in shieldedSpend)
assert('rk' in shieldedSpend)
assert('proof' in shieldedSpend)
assert('spendAuthSig' in shieldedSpend)
shieldedOutput = resp['vShieldedOutput'][0]
assert('cv' in shieldedOutput)
assert('cmu' in shieldedOutput)
assert('ephemeralKey' in shieldedOutput)
assert('encCiphertext' in shieldedOutput)
assert('outCiphertext' in shieldedOutput)
assert('proof' in shieldedOutput)
# Verify importing a spending key will update the nullifiers and witnesses correctly
sk0 = self.nodes[0].z_exportkey(saplingAddr0)
self.nodes[2].z_importkey(sk0, "yes")
assert_equal(self.nodes[2].z_getbalance(saplingAddr0), Decimal('10'))
sk1 = self.nodes[1].z_exportkey(saplingAddr1)
self.nodes[2].z_importkey(sk1, "yes")
assert_equal(self.nodes[2].z_getbalance(saplingAddr1), Decimal('5'))
if __name__ == '__main__':
WalletSaplingTest().main()