mirror of
https://github.com/lightningnetwork/lnd.git
synced 2026-08-13 12:32:48 +02:00
itest: cover on-chain interceptor settlement
Add coverage for held forwards that move on chain after the
incoming channel force closes.
The restart case exercises the path where Bob loses the in-memory
held set and contractcourt re-offers the HTLC through the witness
beacon. The no-restart case keeps the original off-chain hold and
proves that settlement must still reach the on-chain resolver.
(cherry picked from commit 9b31ba83ef)
This commit is contained in:
parent
77989b955d
commit
de8ca73610
2 changed files with 174 additions and 0 deletions
|
|
@ -443,6 +443,14 @@ var allTestCases = []*lntest.TestCase{
|
|||
Name: "forward interceptor restart",
|
||||
TestFunc: testForwardInterceptorRestart,
|
||||
},
|
||||
{
|
||||
Name: "forward interceptor on chain settle after restart",
|
||||
TestFunc: testForwardInterceptorOnChainSettleAfterRestart,
|
||||
},
|
||||
{
|
||||
Name: "forward interceptor on chain settle no restart",
|
||||
TestFunc: testForwardInterceptorOnChainSettleNoRestart,
|
||||
},
|
||||
{
|
||||
Name: "invoice HTLC modifier basic",
|
||||
TestFunc: testInvoiceHtlcModifierBasic,
|
||||
|
|
|
|||
|
|
@ -505,6 +505,172 @@ func testForwardInterceptorRestart(ht *lntest.HarnessTest) {
|
|||
)
|
||||
}
|
||||
|
||||
// testForwardInterceptorOnChainSettleAfterRestart tests that an HTLC offered
|
||||
// to the interceptor by the on-chain resolver remains settleable after a new
|
||||
// block is mined. This reproduces the incident path where Bob restarted after
|
||||
// the force-close, so only the on-chain interceptor entry exists.
|
||||
func testForwardInterceptorOnChainSettleAfterRestart(ht *lntest.HarnessTest) {
|
||||
const (
|
||||
chanAmt = btcutil.Amount(300000)
|
||||
invoiceAmt = int64(100000)
|
||||
)
|
||||
|
||||
// Bob requires an interceptor so the forwarded HTLC remains held until
|
||||
// the test explicitly resolves it.
|
||||
p := lntest.OpenChannelParams{Amt: chanAmt}
|
||||
cfgs := [][]string{nil, {"--requireinterceptor"}, nil}
|
||||
chanPoints, nodes := ht.CreateSimpleNetwork(cfgs, p)
|
||||
alice, bob, carol := nodes[0], nodes[1], nodes[2]
|
||||
cpAB := chanPoints[0]
|
||||
|
||||
// Fund Bob so he can publish the on-chain HTLC success sweep once the
|
||||
// interceptor supplies the preimage.
|
||||
ht.FundCoins(btcutil.SatoshiPerBitcoin, bob)
|
||||
|
||||
interceptor, cancelInterceptor := bob.RPC.HtlcInterceptor()
|
||||
|
||||
addResp := carol.RPC.AddInvoice(&lnrpc.Invoice{
|
||||
Value: invoiceAmt,
|
||||
})
|
||||
invoice := carol.RPC.LookupInvoice(addResp.RHash)
|
||||
|
||||
payHash, err := lntypes.MakeHash(invoice.RHash)
|
||||
require.NoError(ht, err)
|
||||
|
||||
req := &routerrpc.SendPaymentRequest{
|
||||
PaymentRequest: invoice.PaymentRequest,
|
||||
FeeLimitMsat: noFeeLimitMsat,
|
||||
}
|
||||
ht.SendPaymentAssertInflight(alice, req)
|
||||
|
||||
_ = ht.ReceiveHtlcInterceptor(interceptor)
|
||||
ht.AssertIncomingHTLCActive(bob, cpAB, invoice.RHash)
|
||||
ht.AssertPaymentStatus(alice, payHash, lnrpc.Payment_IN_FLIGHT)
|
||||
|
||||
closeStream, _ := ht.CloseChannelAssertPending(
|
||||
alice, cpAB, true,
|
||||
)
|
||||
ht.AssertStreamChannelForceClosed(
|
||||
alice, cpAB, false, closeStream,
|
||||
)
|
||||
ht.AssertChannelPendingForceClose(bob, cpAB)
|
||||
|
||||
cancelInterceptor()
|
||||
ht.RestartNode(bob)
|
||||
|
||||
// Re-register the interceptor after restart. The previous stream was
|
||||
// cancelled before Bob went down. The incoming contest resolver only
|
||||
// re-offers the on-chain HTLC to the active stream.
|
||||
interceptor, cancelInterceptor = bob.RPC.HtlcInterceptor()
|
||||
defer cancelInterceptor()
|
||||
|
||||
// After restart, the incoming contest resolver re-offers the HTLC to
|
||||
// the interceptor through the on-chain path.
|
||||
intercepted := ht.ReceiveHtlcInterceptor(interceptor)
|
||||
|
||||
// Mine one block after the on-chain intercept has been offered. With
|
||||
// the current bug, the held entry is evicted here because the on-chain
|
||||
// packet has no auto-fail height.
|
||||
ht.MineEmptyBlocks(1)
|
||||
|
||||
ht.AssertNumTxsInMempool(0)
|
||||
err = interceptor.Send(&routerrpc.ForwardHtlcInterceptResponse{
|
||||
IncomingCircuitKey: intercepted.IncomingCircuitKey,
|
||||
Action: routerrpc.ResolveHoldForwardAction_SETTLE,
|
||||
Preimage: invoice.RPreimage,
|
||||
})
|
||||
require.NoError(ht, err, "failed to settle intercepted HTLC")
|
||||
|
||||
// The preimage should reach the contest resolver and register Bob's
|
||||
// HTLC success input with the sweeper.
|
||||
ht.AssertAtLeastNumPendingSweeps(bob, 1)
|
||||
|
||||
// Give the sweeper another blockbeat to publish the sweep transaction.
|
||||
ht.MineEmptyBlocks(1)
|
||||
|
||||
ht.MineBlocksAndAssertNumTxes(1, 1)
|
||||
ht.AssertPaymentStatus(alice, payHash, lnrpc.Payment_SUCCEEDED)
|
||||
|
||||
// Bob's sweep is mined above. Clean up Alice's force close so the next
|
||||
// test starts with an empty mempool.
|
||||
ht.CleanupForceClose(alice)
|
||||
}
|
||||
|
||||
// testForwardInterceptorOnChainSettleNoRestart tests that an HTLC which was
|
||||
// first held off-chain can still be settled after the incoming channel
|
||||
// force-closes without restarting Bob. This covers the duplicate-entry path:
|
||||
// the old off-chain held entry must not prevent settlement from reaching the
|
||||
// on-chain contest resolver.
|
||||
func testForwardInterceptorOnChainSettleNoRestart(ht *lntest.HarnessTest) {
|
||||
const (
|
||||
chanAmt = btcutil.Amount(300000)
|
||||
invoiceAmt = int64(100000)
|
||||
)
|
||||
|
||||
// Bob requires an interceptor so the forwarded HTLC remains held until
|
||||
// the test explicitly resolves it.
|
||||
p := lntest.OpenChannelParams{Amt: chanAmt}
|
||||
cfgs := [][]string{nil, {"--requireinterceptor"}, nil}
|
||||
chanPoints, nodes := ht.CreateSimpleNetwork(cfgs, p)
|
||||
alice, bob, carol := nodes[0], nodes[1], nodes[2]
|
||||
cpAB := chanPoints[0]
|
||||
|
||||
// Fund Bob so he can publish the on-chain HTLC success sweep once the
|
||||
// interceptor supplies the preimage.
|
||||
ht.FundCoins(btcutil.SatoshiPerBitcoin, bob)
|
||||
|
||||
interceptor, cancelInterceptor := bob.RPC.HtlcInterceptor()
|
||||
defer cancelInterceptor()
|
||||
|
||||
addResp := carol.RPC.AddInvoice(&lnrpc.Invoice{
|
||||
Value: invoiceAmt,
|
||||
})
|
||||
invoice := carol.RPC.LookupInvoice(addResp.RHash)
|
||||
|
||||
payHash, err := lntypes.MakeHash(invoice.RHash)
|
||||
require.NoError(ht, err)
|
||||
|
||||
req := &routerrpc.SendPaymentRequest{
|
||||
PaymentRequest: invoice.PaymentRequest,
|
||||
FeeLimitMsat: noFeeLimitMsat,
|
||||
}
|
||||
ht.SendPaymentAssertInflight(alice, req)
|
||||
|
||||
intercepted := ht.ReceiveHtlcInterceptor(interceptor)
|
||||
ht.AssertIncomingHTLCActive(bob, cpAB, invoice.RHash)
|
||||
ht.AssertPaymentStatus(alice, payHash, lnrpc.Payment_IN_FLIGHT)
|
||||
|
||||
closeStream, _ := ht.CloseChannelAssertPending(
|
||||
alice, cpAB, true,
|
||||
)
|
||||
ht.AssertStreamChannelForceClosed(
|
||||
alice, cpAB, false, closeStream,
|
||||
)
|
||||
ht.AssertChannelPendingForceClose(bob, cpAB)
|
||||
|
||||
ht.AssertNumTxsInMempool(0)
|
||||
err = interceptor.Send(&routerrpc.ForwardHtlcInterceptResponse{
|
||||
IncomingCircuitKey: intercepted.IncomingCircuitKey,
|
||||
Action: routerrpc.ResolveHoldForwardAction_SETTLE,
|
||||
Preimage: invoice.RPreimage,
|
||||
})
|
||||
require.NoError(ht, err, "failed to settle intercepted HTLC")
|
||||
|
||||
// The preimage should reach the contest resolver and register Bob's
|
||||
// HTLC success input with the sweeper.
|
||||
ht.AssertAtLeastNumPendingSweeps(bob, 1)
|
||||
|
||||
// Give the sweeper another blockbeat to publish the sweep transaction.
|
||||
ht.MineEmptyBlocks(1)
|
||||
|
||||
ht.MineBlocksAndAssertNumTxes(1, 1)
|
||||
ht.AssertPaymentStatus(alice, payHash, lnrpc.Payment_SUCCEEDED)
|
||||
|
||||
// Bob's sweep is mined above. Clean up Alice's force close so the next
|
||||
// test starts with an empty mempool.
|
||||
ht.CleanupForceClose(alice)
|
||||
}
|
||||
|
||||
// interceptorTestScenario is a helper struct to hold the test context and
|
||||
// provide the needed functionality.
|
||||
type interceptorTestScenario struct {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue