Skip to content

Commit d179903

Browse files
committed
lntest: add new utilities to wait for mempool/block inclusion, then trigger sweep if failed
This implements a generalized pattern where we'll try out assertion, then if that fails, we'll try a sweep, then try the assertion again. This uses the new TriggerSweep call that was added earlier.
1 parent 503dd29 commit d179903

File tree

3 files changed

+132
-0
lines changed

3 files changed

+132
-0
lines changed

lntest/harness_miner.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,37 @@ func (h *HarnessTest) MineBlocksAndAssertNumTxes(num uint32,
121121
return blocks
122122
}
123123

124+
// MineBlocksAndAssertNumTxesWithSweep is like MineBlocksAndAssertNumTxes but
125+
// handles async confirmation notification races by triggering sweeps if needed.
126+
// Use this for tests that expect sweep transactions after force closes or other
127+
// events where confirmation notifications may arrive asynchronously.
128+
func (h *HarnessTest) MineBlocksAndAssertNumTxesWithSweep(num uint32,
129+
numTxs int, hn *node.HarnessNode) []*wire.MsgBlock {
130+
131+
// Update the harness's current height.
132+
defer h.updateCurrentHeight()
133+
134+
// Wait for transactions with sweep triggering support.
135+
txids := h.AssertNumTxsInMempoolWithSweepTrigger(numTxs, hn)
136+
137+
// Mine blocks.
138+
blocks := h.miner.MineBlocks(num)
139+
140+
// Assert that all the transactions were included in the first block.
141+
for _, txid := range txids {
142+
h.miner.AssertTxInBlock(blocks[0], txid)
143+
}
144+
145+
// Make sure the mempool has been updated.
146+
h.miner.AssertTxnsNotInMempool(txids)
147+
148+
// Finally, make sure all the active nodes are synced.
149+
bestBlock := blocks[len(blocks)-1]
150+
h.AssertActiveNodesSyncedTo(bestBlock.BlockHash())
151+
152+
return blocks
153+
}
154+
124155
// ConnectMiner connects the miner with the chain backend in the network.
125156
func (h *HarnessTest) ConnectMiner() {
126157
err := h.manager.chainBackend.ConnectMiner()
@@ -222,6 +253,16 @@ func (h *HarnessTest) AssertNumTxsInMempool(n int) []chainhash.Hash {
222253
return h.miner.AssertNumTxsInMempool(n)
223254
}
224255

256+
// AssertNumTxsInMempoolWithSweepTrigger waits for N transactions with sweep
257+
// triggering support to handle async confirmation notification races. If
258+
// transactions don't appear within a short timeout, it triggers a manual sweep
259+
// via the provided node's RPC and waits again.
260+
func (h *HarnessTest) AssertNumTxsInMempoolWithSweepTrigger(n int,
261+
hn *node.HarnessNode) []chainhash.Hash {
262+
263+
return h.miner.AssertNumTxsInMempoolWithSweepTrigger(n, hn.RPC)
264+
}
265+
225266
// AssertOutpointInMempool asserts a given outpoint can be found in the mempool.
226267
func (h *HarnessTest) AssertOutpointInMempool(op wire.OutPoint) *wire.MsgTx {
227268
return h.miner.AssertOutpointInMempool(op)
@@ -240,6 +281,23 @@ func (h *HarnessTest) GetNumTxsFromMempool(n int) []*wire.MsgTx {
240281
return h.miner.GetNumTxsFromMempool(n)
241282
}
242283

284+
// GetNumTxsFromMempoolWithSweep gets N transactions from mempool with sweep
285+
// triggering support to handle async confirmation notification races. Use this
286+
// for tests that expect sweep transactions after force closes.
287+
func (h *HarnessTest) GetNumTxsFromMempoolWithSweep(n int,
288+
hn *node.HarnessNode) []*wire.MsgTx {
289+
290+
txids := h.AssertNumTxsInMempoolWithSweepTrigger(n, hn)
291+
292+
var txes []*wire.MsgTx
293+
for _, txid := range txids {
294+
tx := h.miner.GetRawTransaction(txid)
295+
txes = append(txes, tx.MsgTx())
296+
}
297+
298+
return txes
299+
}
300+
243301
// GetBestBlock makes a RPC request to miner and asserts.
244302
func (h *HarnessTest) GetBestBlock() (*chainhash.Hash, int32) {
245303
return h.miner.GetBestBlock()

lntest/miner/miner.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/btcsuite/btcd/rpcclient"
1919
"github.com/btcsuite/btcd/wire"
2020
"github.com/lightningnetwork/lnd/fn/v2"
21+
"github.com/lightningnetwork/lnd/lnrpc/devrpc"
2122
"github.com/lightningnetwork/lnd/lntest/node"
2223
"github.com/lightningnetwork/lnd/lntest/wait"
2324
"github.com/stretchr/testify/require"
@@ -225,6 +226,64 @@ func (h *HarnessMiner) AssertNumTxsInMempool(n int) []chainhash.Hash {
225226
return mem
226227
}
227228

229+
// SweeperTrigger is an interface that allows triggering a manual sweep.
230+
type SweeperTrigger interface {
231+
TriggerSweeper(*devrpc.TriggerSweeperRequest) *devrpc.TriggerSweeperResponse
232+
}
233+
234+
// AssertNumTxsInMempoolWithSweepTrigger waits for N transactions to appear in
235+
// the mempool. If they don't appear within a short timeout, it triggers a
236+
// manual sweep via the provided node's RPC and waits again. This handles the
237+
// async confirmation notification race where sweeps may be registered after
238+
// block processing completes.
239+
func (h *HarnessMiner) AssertNumTxsInMempoolWithSweepTrigger(n int,
240+
node SweeperTrigger) []chainhash.Hash {
241+
242+
// First, try the fast path with a shorter timeout (5 seconds). Most
243+
// tests should hit this path when confirmation notifications arrive
244+
// quickly.
245+
shortTimeout := 5 * time.Second
246+
var mem []chainhash.Hash
247+
248+
err := wait.NoError(func() error {
249+
mem = h.GetRawMempool()
250+
if len(mem) == n {
251+
return nil
252+
}
253+
254+
return fmt.Errorf("want %v, got %v in mempool: %v",
255+
n, len(mem), mem)
256+
}, shortTimeout)
257+
258+
// If we found the transactions quickly, we're done.
259+
if err == nil {
260+
return mem
261+
}
262+
263+
// Second, short timeout expired. This likely means confirmation
264+
// notifications haven't arrived yet due to async processing. Trigger a
265+
// manual sweep to force broadcast of any registered sweeps.
266+
h.Logf("AssertNumTxsInMempoolWithSweepTrigger: timeout after %v, "+
267+
"triggering sweep (want %d, got %d)", shortTimeout, n, len(mem))
268+
269+
node.TriggerSweeper(&devrpc.TriggerSweeperRequest{})
270+
271+
// Next, we'll wait again with the full timeout. The sweep should now be
272+
// broadcast if confirmation notifications have arrived.
273+
err = wait.NoError(func() error {
274+
mem = h.GetRawMempool()
275+
if len(mem) == n {
276+
return nil
277+
}
278+
279+
return fmt.Errorf("want %v, got %v in mempool: %v",
280+
n, len(mem), mem)
281+
}, wait.MinerMempoolTimeout)
282+
require.NoError(h, err, "assert tx in mempool timeout after trigger")
283+
284+
return mem
285+
}
286+
228287
// AssertTxInBlock asserts that a given txid can be found in the passed block.
229288
func (h *HarnessMiner) AssertTxInBlock(block *wire.MsgBlock,
230289
txid chainhash.Hash) {

lntest/rpc/lnd.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,21 @@ func (h *HarnessRPC) Quiesce(
816816
return res
817817
}
818818

819+
// TriggerSweeper triggers an immediate sweep of all pending inputs. This is
820+
// useful for tests to deterministically control when sweeps are broadcast,
821+
// especially when handling async confirmation notification races.
822+
func (h *HarnessRPC) TriggerSweeper(
823+
req *devrpc.TriggerSweeperRequest) *devrpc.TriggerSweeperResponse {
824+
825+
ctx, cancel := context.WithTimeout(h.runCtx, DefaultTimeout)
826+
defer cancel()
827+
828+
res, err := h.Dev.TriggerSweeper(ctx, req)
829+
h.NoError(err, "TriggerSweeper returned an error")
830+
831+
return res
832+
}
833+
819834
type PeerEventsClient lnrpc.Lightning_SubscribePeerEventsClient
820835

821836
// SubscribePeerEvents makes a RPC call to the node's SubscribePeerEvents and

0 commit comments

Comments
 (0)