From 698acf98c51554568789edc5320b005e3434a1a5 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Tue, 10 Mar 2026 03:58:50 +0000 Subject: [PATCH 1/4] Migrate to `getwalletstatus` RPC for syncing wallets --- qa/rpc-tests/test_framework/test_framework.py | 19 ++---- qa/rpc-tests/test_framework/util.py | 65 +++++++++++++++---- 2 files changed, 58 insertions(+), 26 deletions(-) diff --git a/qa/rpc-tests/test_framework/test_framework.py b/qa/rpc-tests/test_framework/test_framework.py index 52e23bf1c..9f906bc14 100755 --- a/qa/rpc-tests/test_framework/test_framework.py +++ b/qa/rpc-tests/test_framework/test_framework.py @@ -12,7 +12,6 @@ import sys import shutil import tempfile -import time import traceback from .config import ZebraArgs @@ -131,21 +130,15 @@ def split_network(self): def sync_all(self, do_mempool_sync = True): if self.is_network_split: - sync_blocks(self.nodes[:2]) - sync_blocks(self.nodes[2:]) + sync_blocks(self.nodes[:2], None if self.wallets is None else self.wallets[:2]) + sync_blocks(self.nodes[2:], None if self.wallets is None else self.wallets[2:]) if do_mempool_sync: - sync_mempools(self.nodes[:2]) - sync_mempools(self.nodes[2:]) + sync_mempools(self.nodes[:2], None if self.wallets is None else self.wallets[:2]) + sync_mempools(self.nodes[2:], None if self.wallets is None else self.wallets[2:]) else: - sync_blocks(self.nodes) + sync_blocks(self.nodes, self.wallets) if do_mempool_sync: - sync_mempools(self.nodes) - - # TODO: Sync wallets inside `sync_blocks` - # TODO: Use `getwalletstatus` in all sync issues - # https://github.com/zcash/wallet/issues/316 - if self.num_wallets > 0: - time.sleep(2) + sync_mempools(self.nodes, self.wallets) def join_network(self): """ diff --git a/qa/rpc-tests/test_framework/util.py b/qa/rpc-tests/test_framework/util.py index 27ca9f3b3..895be95a7 100644 --- a/qa/rpc-tests/test_framework/util.py +++ b/qa/rpc-tests/test_framework/util.py @@ -161,7 +161,7 @@ def hex_str_to_bytes(hex_str): def str_to_b64str(string): return b64encode(string.encode('utf-8')).decode('ascii') -def sync_blocks(rpc_connections, wait=0.125, timeout=60, allow_different_tips=False): +def sync_blocks(nodes, wallets=None, wait=0.125, timeout=60, allow_different_tips=False): """ Wait until everybody has the same tip, and has notified all internal listeners of them. @@ -171,31 +171,67 @@ def sync_blocks(rpc_connections, wait=0.125, timeout=60, allow_different_tips=Fa """ while timeout > 0: if allow_different_tips: - tips = [ x.getblockcount() for x in rpc_connections ] + tips = [ x.getblockcount() for x in nodes ] else: - tips = [ x.getbestblockhash() for x in rpc_connections ] + tips = [ x.getbestblockhash() for x in nodes ] if tips == [ tips[0] ]*len(tips): + if not wallets: + return True break time.sleep(wait) timeout -= wait - return True -def sync_mempools(rpc_connections, wait=0.5, timeout=60): + if not not wallets: + # Now that the block counts are in sync, wait for the internal + # notifications to finish + while timeout > 0: + wallet_status = [ x.getwalletstatus() for x in wallets ] + if allow_different_tips: + wallet_node_tips = [ x['node_tip']['height'] for x in wallet_status ] + wallet_tips = [ x['wallet_tip']['height'] for x in wallet_status ] + else: + wallet_node_tips = [ x['node_tip']['blockhash'] for x in wallet_status ] + wallet_tips = [ x['wallet_tip']['blockhash'] for x in wallet_status ] + if tips == wallet_node_tips and tips == wallet_tips: + return True + time.sleep(wait) + timeout -= wait + + print('Node tips:', tips) + print('Wallet statuses:', wallet_status) + raise AssertionError("Block sync failed") + +def sync_mempools(nodes, wallets=None, wait=0.5, timeout=60): """ Wait until everybody has the same transactions in their memory pools, and has notified all internal listeners of them """ while timeout > 0: - pool = set(rpc_connections[0].getrawmempool()) + pool = set(nodes[0].getrawmempool()) num_match = 1 - for i in range(1, len(rpc_connections)): - if set(rpc_connections[i].getrawmempool()) == pool: + for i in range(1, len(nodes)): + if set(nodes[i].getrawmempool()) == pool: num_match = num_match+1 - if num_match == len(rpc_connections): + if num_match == len(nodes): + if not wallets: + return True break time.sleep(wait) timeout -= wait - return True + + if not not wallets: + # Now that the mempools are in sync, wait for the internal + # notifications to finish + while timeout > 0: + tips = [ x.getwalletstatus() for x in wallets ] + tips = [ (x['node_tip']['blockhash'], x['wallet_tip']['blockhash']) for x in tips ] + if tips == [ tips[0] ]*len(tips) and tips[0][0] == tips[0][1]: + return True + time.sleep(wait) + timeout -= wait + + print('Wallet view of tips:', tips) + raise AssertionError("Mempool sync failed") bitcoind_processes = {} @@ -439,9 +475,12 @@ def rebuild_cache(): sys.exit(1) # Wait for zallets to synchronize with the nodes - # TODO: Use `getwalletstatus` in all sync issues - # https://github.com/zcash/wallet/issues/316 - time.sleep(10) + while True: + tips = [ x.getwalletstatus() for x in wallets ] + tips = [ (x['node_tip']['blockhash'], x['wallet_tip']['blockhash']) for x in tips ] + if tips == [ tips[0] ]*len(tips) and tips[0][0] == tips[0][1]: + break + time.sleep(0.25) # Shut them down, and clean up cache directories: stop_wallets(wallets) From b0eade2dc44be424eb9ce765fc66d4eb0ca3863d Mon Sep 17 00:00:00 2001 From: Alfredo Garcia Date: Fri, 5 Jun 2026 07:36:00 -0300 Subject: [PATCH 2/4] test framework: handle absent getwalletstatus wallet_tip; sync wallets in wallet.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The getwalletstatus sync barrier indexed `wallet_tip` unconditionally, but the RPC omits that field (Option + skip_serializing_if) until the wallet has a committed tip — which happens transiently right after blocks are mined. The barrier therefore raised `KeyError: 'wallet_tip'` ~1 run in 4, exactly during the window it is meant to poll through. Treat an absent wallet_tip as "not synced yet" and keep polling, in sync_blocks, sync_mempools, and rebuild_cache. Also make wallet.py use the now wallet-aware self.sync_all() instead of an immediate read and a fixed time.sleep(1), removing the remaining flaky wallet-scan race (and the unused `import time`). This supersedes the bespoke pollers in #106. Validated 10/10 green locally against a zallet carrying zcash/wallet#367 (getwalletstatus) + zcash/wallet#455 (empty shielded tree fix). Closes #105 Co-Authored-By: Claude Opus 4.8 (1M context) --- qa/rpc-tests/test_framework/util.py | 45 ++++++++++++++++------------- qa/rpc-tests/wallet.py | 8 +++-- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/qa/rpc-tests/test_framework/util.py b/qa/rpc-tests/test_framework/util.py index 895be95a7..b6a2acff3 100644 --- a/qa/rpc-tests/test_framework/util.py +++ b/qa/rpc-tests/test_framework/util.py @@ -183,17 +183,17 @@ def sync_blocks(nodes, wallets=None, wait=0.125, timeout=60, allow_different_tip if not not wallets: # Now that the block counts are in sync, wait for the internal - # notifications to finish + # notifications to finish. `getwalletstatus` omits `wallet_tip` until + # the wallet has a committed tip, so treat its absence as "not synced + # yet" and keep polling instead of raising KeyError. while timeout > 0: wallet_status = [ x.getwalletstatus() for x in wallets ] - if allow_different_tips: - wallet_node_tips = [ x['node_tip']['height'] for x in wallet_status ] - wallet_tips = [ x['wallet_tip']['height'] for x in wallet_status ] - else: - wallet_node_tips = [ x['node_tip']['blockhash'] for x in wallet_status ] - wallet_tips = [ x['wallet_tip']['blockhash'] for x in wallet_status ] - if tips == wallet_node_tips and tips == wallet_tips: - return True + if all('wallet_tip' in w for w in wallet_status): + key = 'height' if allow_different_tips else 'blockhash' + wallet_node_tips = [ w['node_tip'][key] for w in wallet_status ] + wallet_tips = [ w['wallet_tip'][key] for w in wallet_status ] + if tips == wallet_node_tips and tips == wallet_tips: + return True time.sleep(wait) timeout -= wait @@ -221,16 +221,19 @@ def sync_mempools(nodes, wallets=None, wait=0.5, timeout=60): if not not wallets: # Now that the mempools are in sync, wait for the internal - # notifications to finish + # notifications to finish. `getwalletstatus` omits `wallet_tip` until + # the wallet has a committed tip, so treat its absence as "not synced + # yet" and keep polling instead of raising KeyError. while timeout > 0: - tips = [ x.getwalletstatus() for x in wallets ] - tips = [ (x['node_tip']['blockhash'], x['wallet_tip']['blockhash']) for x in tips ] - if tips == [ tips[0] ]*len(tips) and tips[0][0] == tips[0][1]: - return True + wallet_status = [ x.getwalletstatus() for x in wallets ] + if all('wallet_tip' in w for w in wallet_status): + tips = [ (w['node_tip']['blockhash'], w['wallet_tip']['blockhash']) for w in wallet_status ] + if tips == [ tips[0] ]*len(tips) and tips[0][0] == tips[0][1]: + return True time.sleep(wait) timeout -= wait - print('Wallet view of tips:', tips) + print('Wallet view of tips:', wallet_status) raise AssertionError("Mempool sync failed") bitcoind_processes = {} @@ -474,12 +477,14 @@ def rebuild_cache(): sys.stderr.write("Error connecting to "+rpc_url_wallet(i)+"\n") sys.exit(1) - # Wait for zallets to synchronize with the nodes + # Wait for zallets to synchronize with the nodes. `getwalletstatus` + # omits `wallet_tip` until the wallet has a committed tip. while True: - tips = [ x.getwalletstatus() for x in wallets ] - tips = [ (x['node_tip']['blockhash'], x['wallet_tip']['blockhash']) for x in tips ] - if tips == [ tips[0] ]*len(tips) and tips[0][0] == tips[0][1]: - break + wallet_status = [ x.getwalletstatus() for x in wallets ] + if all('wallet_tip' in w for w in wallet_status): + tips = [ (w['node_tip']['blockhash'], w['wallet_tip']['blockhash']) for w in wallet_status ] + if tips == [ tips[0] ]*len(tips) and tips[0][0] == tips[0][1]: + break time.sleep(0.25) # Shut them down, and clean up cache directories: diff --git a/qa/rpc-tests/wallet.py b/qa/rpc-tests/wallet.py index c871395f2..ad58e55cd 100755 --- a/qa/rpc-tests/wallet.py +++ b/qa/rpc-tests/wallet.py @@ -5,7 +5,6 @@ # file COPYING or https://www.opensource.org/licenses/mit-license.php . #from decimal import Decimal -import time from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, assert_true @@ -26,6 +25,9 @@ def run_test(self): node_balance = self.nodes[0].getaddressbalance(transparent_address) assert_equal(node_balance['balance'], 625000000) + # Wait for the wallet to scan and commit the coinbase block. + self.sync_all() + # Zallet can see the balance. wallet_balance = self.wallets[0].z_gettotalbalance(1, True) # TODO: Result is a string (https://github.com/zcash/wallet/issues/15) @@ -34,8 +36,8 @@ def run_test(self): # Mine another block self.nodes[0].generate(1) - # Wait for the wallet to sync - time.sleep(1) + # Wait for the wallet to scan and commit the new block. + self.sync_all() node_balance = self.nodes[0].getaddressbalance(transparent_address) assert_equal(node_balance['balance'], 1250000000) From 033f127dcc1bcbe0eb10a0b661b5c9ea7838336e Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Wed, 10 Jun 2026 21:50:15 +0200 Subject: [PATCH 3/4] qa/util: fix conditional when no wallets given in sync_mempools --- qa/rpc-tests/test_framework/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qa/rpc-tests/test_framework/util.py b/qa/rpc-tests/test_framework/util.py index b6a2acff3..79b17dea8 100644 --- a/qa/rpc-tests/test_framework/util.py +++ b/qa/rpc-tests/test_framework/util.py @@ -181,7 +181,7 @@ def sync_blocks(nodes, wallets=None, wait=0.125, timeout=60, allow_different_tip time.sleep(wait) timeout -= wait - if not not wallets: + if wallets: # Now that the block counts are in sync, wait for the internal # notifications to finish. `getwalletstatus` omits `wallet_tip` until # the wallet has a committed tip, so treat its absence as "not synced @@ -219,7 +219,7 @@ def sync_mempools(nodes, wallets=None, wait=0.5, timeout=60): time.sleep(wait) timeout -= wait - if not not wallets: + if wallets: # Now that the mempools are in sync, wait for the internal # notifications to finish. `getwalletstatus` omits `wallet_tip` until # the wallet has a committed tip, so treat its absence as "not synced From 4cf2be84ef00333f1377bd8f1ff952a2c8efb090 Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Wed, 10 Jun 2026 21:47:17 +0200 Subject: [PATCH 4/4] qa/util: improve documentation of sync_mempools --- qa/rpc-tests/test_framework/util.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qa/rpc-tests/test_framework/util.py b/qa/rpc-tests/test_framework/util.py index 79b17dea8..99b12d909 100644 --- a/qa/rpc-tests/test_framework/util.py +++ b/qa/rpc-tests/test_framework/util.py @@ -205,6 +205,8 @@ def sync_mempools(nodes, wallets=None, wait=0.5, timeout=60): """ Wait until everybody has the same transactions in their memory pools, and has notified all internal listeners of them + + Returns `True` when all wallets are in synced, or if no wallet is given. """ while timeout > 0: pool = set(nodes[0].getrawmempool())