Skip to content

initialize_chain hangs forever: no-timeout wallet-sync loop + zebrad nodes left at divergent heights after final restart #136

Description

@zancas

Summary

full_test_suite.py (and any rpc-test using the cached chain) hangs forever during initialize_chain cache generation. The wait loop in qa/rpc-tests/test_framework/util.py that blocks on all wallets reaching the node tip has no timeout, and the 8 zebrad nodes it waits on end up stuck at divergent block heights that never converge — so the loop can never exit and the suite hangs instead of failing.

Symptom

Running:

uv run ./qa/zcash/full_test_suite.py

hangs with zallet repeatedly logging Reached chain tip, streaming mempool and re-scanning the same block range, with no further progress and no error.

Diagnosis

The hang is in initialize_chain, in the final wallet-sync wait:

https://github.com/zcash/integration-tests/blob/main/qa/rpc-tests/test_framework/util.py#L485

# Wait for zallets to synchronize with the nodes.
while True:                                    # <-- no timeout
    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)

This loop only breaks when all 8 wallets share one identical node_tip (tips == [tips[0]]*len(tips)) and each wallet's wallet_tip == node_tip. Unlike sync_blocks/sync_mempools (which have a 60s timeout and raise AssertionError), this one has no escape hatch.

Live evidence

Probing getwalletstatus on the hung run (5 samples, 1s apart — all stable):

wallet node_tip.height wallet_tip.height
0 162 162
1 162 162
2 162 162
3 180 180
4 177 177
5 176 176
6 176 176
7 186 186

Each wallet's wallet_tip exactly matches its own node_tip and is stable — i.e. the wallets are fully synced; zallet is not the problem. The 8 zebrad nodes are stuck at different heights (162–186) and never converge, so the all-equal condition can never hold.

Why the nodes diverge

The block-generation loop mines on one peer, then stops and restarts all 8 zebrad nodes every round as a workaround for ZcashFoundation/zebra#10329 / #10332 ("zebrad won't broadcast received blocks to other connected nodes"):

https://github.com/zcash/integration-tests/blob/main/qa/rpc-tests/test_framework/util.py#L424-L459

for i in range(2):
    for peer in range(4):
        # Connect the other nodes to the mining peer.
        for i in range(0, MAX_NODES):
            if i != peer:
                connect_nodes_bi(rpcs, i, peer)
        # Mine the blocks
        for j in range(25):
            rpcs[peer].generate(1)
            block_time += PRE_BLOSSOM_BLOCK_TARGET_SPACING
        # Must sync before next peer starts generating blocks
        sync_blocks(rpcs)
        # Shut down and restart every zebrad node. (workaround for zebra#10329/#10332)
        stop_nodes(rpcs)
        wait_bitcoinds()
        for i in range(MAX_NODES):
            ...restart each node...
        for i in range(MAX_NODES):
            rpcs.append(get_rpc_proxy(rpc_url(i), i))   # (1) appends instead of replacing

Two structural problems compound here:

  1. No reconnect / final sync_blocks after the last restart. connect_nodes_bi runs only at the top of each round. After the final round's stop_nodes + restart, the nodes come back up not peered to each other and with no final sync_blocks — so they sit at divergent heights forever. (Confirmed: there is no connect_nodes/sync_blocks/generate between the loop and the wallet startup at lines 460–485.) This likely interacts with the ephemeral node state ([state] cache_dir = "", [network] cache_dir = false in qa/defaults/zebrad/config.toml): on restart each node has nothing to re-sync from.

  2. rpcs.append(...) grows the list unbounded (line marked (1)) instead of replacing it — after 8 restarts rpcs holds ~72 proxy objects (all pointing at the same 8 ports). Latent, but it makes sync_blocks(rpcs) and rpcs[peer] increasingly confusing.

Note: the exact mechanism by which the heights end up different (rather than equal-but-isolated) after the final restart still needs confirmation — it may be the ephemeral-state + no-reconnect interaction above, or a deeper restart/IBD issue. The two facts that are fully verified: (a) the nodes are stable at divergent heights, and (b) the wait loop has no timeout so the suite hangs rather than failing.

Suggested fixes

  1. Give the :485 loop a timeout (mirroring sync_blocks) so a non-converging chain fails loudly with the tip table printed, instead of hanging. This alone makes every future occurrence debuggable.
  2. Reconnect + sync_blocks(rpcs) after the final restart (or restructure so the last round ends in a converged, synced state) so the 8 nodes actually share a tip before the wallets start.
  3. Replace, don't append, rpcs after each restart (rpcs[:] = [...] or rebuild the list).

Environment

  • integration-tests @ 175585714 (main)
  • zallet 0.1.0-alpha.4, built from zallet main
  • zebrad / zainod from current main
  • Linux x86_64

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions