Skip to content

dualopend: reconnect during block catch-up locks in the wrong RBF inflight #9373

Description

@ksedgwic

If a non-tip inflight is the one that gets mined, and the peer reconnects while we are still catching up on blocks, we lock the channel in with the tip inflight -- which was never mined.

  1. The block containing the mined inflight is processed. dual_funding_found fires inline and sets channel->scid, but update_channel_from_inflight runs later, from the blockdepth watches in updates_complete, only once we have caught up on all queued blocks. Until then channel->funding still points at the tip inflight.

  2. The peer reconnects inside that window. peer_restart_dualopend reinits dualopend with channel_current_inflight() -- the tip -- plus channel->scid != NULL, which dualopend takes as funding-is-confirmed. It sends channel_ready.

  3. The peer, who identified the mined inflight correctly, sends channel_ready back. handle_channel_locked promotes to CHANNELD_NORMAL with the tip inflight's funding and clears the inflight list, discarding the pending correction.

  4. We are now in CHANNELD_NORMAL with a funding_txid that was never mined, an scid that points at the other tx's outpoint, and a last_tx that spends a nonexistent outpoint -- nothing to enforce onchain -- while the peer locked in the mined inflight, so the two sides also disagree with each other.

The window is one block-fetch wide, so any reconnect during catch-up can hit it. All it takes to set it up is an RBF replacement that failed to propagate, leaving a non-tip inflight as the one the chain picks.

We can reproduce this deterministically: stall the node's getblockhash for the block after the funding block (holding the catch-up window open) and reconnect. Fails the same way every run, in about 15 seconds. PR with the regression test and a proposed fix to follow.


Addendum: how it surfaced. Master run for 8c42be3, Test CLN dual-fund Full Integration:

FAILED tests/test_opening.py::test_rbf_non_last_mined - AssertionError: assert 'd6ef2bda0490820a06461cd63d6bb75d4ba11fe2e58b9e61935d7ea904d2b3b5' == '597180eccc316cec5b817794e8f29d848b4b8364762a8e1e567f3bc7bc1e8f8a'

https://github.com/ElementsProject/lightning/actions/runs/30835065159 (logs expire, key lines below). 597180 was the mined inflight; d6ef2b was the tip, never in the mempool. l1:

17:56:23.995 lightningd: Adding block 103: 4ab240...
17:56:23.997 wallet: Owning output 0 17667576sat (p2tr) txid 597180ec... CONFIRMED
17:56:24.442 chan#1: Peer has reconnected, state DUALOPEND_AWAITING_LOCKIN: connecting subd
17:56:24.515 chan#1: State changed from DUALOPEND_AWAITING_LOCKIN to CHANNELD_NORMAL
17:56:24.602 chan#1: Funding tx d6ef2bda... depth 6 of 1
17:56:24.617 channeld-chan#1: handle_funding_depth: Setting short_channel_ids[LOCAL] to 103x1x1

(103x1x1 is 597180's funding outpoint -- its funding output is vout 1; d6ef2b's is vout 0.)

Part of #9222.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions