diff --git a/contractcourt/htlc_incoming_contest_resolver.go b/contractcourt/htlc_incoming_contest_resolver.go index 70349808c6..262ebd01e8 100644 --- a/contractcourt/htlc_incoming_contest_resolver.go +++ b/contractcourt/htlc_incoming_contest_resolver.go @@ -90,9 +90,13 @@ func (h *htlcIncomingContestResolver) Launch() error { } h.log.Debugf("launching contest resolver...") + _, bestHeight, err := h.ChainIO.GetBestBlock() + if err != nil { + return err + } // Query the preimage and apply it if we already know it. - applied, err := h.findAndapplyPreimage() + applied, err := h.findAndapplyPreimage(bestHeight) if err != nil { return err } @@ -615,7 +619,8 @@ var _ htlcContractResolver = (*htlcIncomingContestResolver)(nil) // // NOTE: Since we have two places to query the preimage, we need to check both // the preimage db and the invoice db to look up the preimage. -func (h *htlcIncomingContestResolver) findAndapplyPreimage() (bool, error) { +func (h *htlcIncomingContestResolver) findAndapplyPreimage( + currentHeight int32) (bool, error) { // Query to see if we already know the preimage. preimage, ok := h.PreimageDB.LookupPreimage(h.htlc.RHash) @@ -658,13 +663,13 @@ func (h *htlcIncomingContestResolver) findAndapplyPreimage() (bool, error) { // immediately, we'll assume we don't know it yet and let the `Resolve` // handle the waiting. // - // NOTE: we use a nil subscriber here and a zero current height as we - // are only interested in the settle resolution. + // NOTE: we use a nil subscriber here as we are only interested in the + // settle resolution. // // TODO(yy): move this logic to link and let the preimage be accessed // via the preimage beacon. resolution, err := h.Registry.NotifyExitHopHtlc( - h.htlc.RHash, h.htlc.Amt, h.htlcExpiry, 0, + h.htlc.RHash, h.htlc.Amt, h.htlcExpiry, currentHeight, circuitKey, nil, h.htlc.CustomRecords, payload, ) if err != nil { diff --git a/contractcourt/htlc_incoming_contest_resolver_test.go b/contractcourt/htlc_incoming_contest_resolver_test.go index f17190e96e..3ca9274bd8 100644 --- a/contractcourt/htlc_incoming_contest_resolver_test.go +++ b/contractcourt/htlc_incoming_contest_resolver_test.go @@ -4,6 +4,7 @@ import ( "bytes" "io" "testing" + "time" "github.com/btcsuite/btcd/wire" sphinx "github.com/lightningnetwork/lightning-onion" @@ -116,6 +117,119 @@ func TestHtlcIncomingResolverFwdTimeout(t *testing.T) { ctx.waitForResult(false) } +// TestHtlcIncomingResolverLaunchUsesKnownPreimageAfterExpiry asserts that +// Launch still offers the HTLC to the sweeper when the preimage was already +// learned before restart, even if the HTLC is now expired. +func TestHtlcIncomingResolverLaunchUsesKnownPreimageAfterExpiry(t *testing.T) { + t.Parallel() + defer timeout()() + + tests := []struct { + name string + isExit bool + setup func(*incomingResolverTestContext) + assert func(*testing.T, *incomingResolverTestContext) + }{ + { + name: "preimage db", + isExit: false, + setup: func(ctx *incomingResolverTestContext) { + ctx.witnessBeacon.lookupPreimage[testResHash] = + testResPreimage + }, + assert: func(t *testing.T, + ctx *incomingResolverTestContext) { + + require.Empty(t, ctx.registry.immediateNotify) + }, + }, + { + name: "invoice registry replay", + isExit: true, + setup: func(ctx *incomingResolverTestContext) { + ctx.registry.notifyResolution = + invoices.NewSettleResolution( + testResPreimage, + testResCircuitKey, + testAcceptHeight, + invoices.ResultReplayToSettled, + ) + }, + assert: func(t *testing.T, + ctx *incomingResolverTestContext) { + + require.Len(t, ctx.registry.immediateNotify, 1) + notify := ctx.registry.immediateNotify[0] + require.Equal( + t, ctx.chainIO.BestHeight, + notify.currentHeight, + ) + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // Arrange: restart at the HTLC expiry with the preimage + // already available from this test's source. + ctx := newIncomingResolverTestContext(t, test.isExit) + ctx.resolver.htlcExpiry = testInitialBlockHeight + test.setup(ctx) + + // Act: launch without entering Resolve's block epoch + // loop. + require.NoError(t, ctx.resolver.Launch()) + + // Assert: the known preimage still starts the direct + // sweep path after expiry. + require.True(t, ctx.resolver.isLaunched()) + preimage := lntypes.Preimage( + ctx.resolver.htlcResolution.Preimage, + ) + require.Equal( + t, testResPreimage, preimage, + ) + + sweeper, ok := ctx.resolver.Sweeper.(*mockSweeper) + require.True(t, ok) + select { + case <-sweeper.sweptInputs: + case <-time.After(time.Second): + t.Fatal("expected known preimage sweep") + } + + test.assert(t, ctx) + }) + } +} + +// TestHtlcIncomingResolverLaunchUsesCurrentHeight asserts that Launch uses the +// current chain height for its immediate invoice-registry lookup instead of the +// historical zero height placeholder. +func TestHtlcIncomingResolverLaunchUsesCurrentHeight(t *testing.T) { + t.Parallel() + defer timeout()() + + // Arrange: set up an exit-hop resolver with a registry resolution that + // Launch can consume immediately. + ctx := newIncomingResolverTestContext(t, true) + ctx.chainIO.BestHeight = testInitialBlockHeight + 1 + ctx.registry.notifyResolution = invoices.NewSettleResolution( + testResPreimage, testResCircuitKey, testAcceptHeight, + invoices.ResultReplayToSettled, + ) + + // Act: launch performs the immediate invoice-registry lookup. + require.NoError(t, ctx.resolver.Launch()) + + // Assert: the registry saw the chain height returned by ChainIO. + require.Len(t, ctx.registry.immediateNotify, 1) + require.Equal( + t, ctx.chainIO.BestHeight, + ctx.registry.immediateNotify[0].currentHeight, + ) +} + // TestHtlcIncomingResolverExitSettle tests resolution of an exit hop htlc for // which the invoice has already been settled when the resolver starts. func TestHtlcIncomingResolverExitSettle(t *testing.T) { @@ -306,6 +420,7 @@ type incomingResolverTestContext struct { witnessBeacon *mockWitnessBeacon resolver *htlcIncomingContestResolver notifier *mock.ChainNotifier + chainIO *mock.ChainIO onionProcessor *mockOnionProcessor resolveErr chan error nextResolver ContractResolver @@ -320,6 +435,9 @@ func newIncomingResolverTestContext(t *testing.T, isExit bool) *incomingResolver ConfChan: make(chan *chainntnfs.TxConfirmation), } witnessBeacon := newMockWitnessBeacon() + chainIO := &mock.ChainIO{ + BestHeight: testInitialBlockHeight, + } registry := &mockRegistry{ notifyChan: make(chan notifyExitHopData, 1), } @@ -332,6 +450,7 @@ func newIncomingResolverTestContext(t *testing.T, isExit bool) *incomingResolver registry: registry, witnessBeacon: witnessBeacon, notifier: notifier, + chainIO: chainIO, onionProcessor: onionProcessor, t: t, } @@ -359,6 +478,7 @@ func newIncomingResolverTestContext(t *testing.T, isExit bool) *incomingResolver return nil }, Sweeper: newMockSweeper(), + ChainIO: chainIO, }, PutResolverReport: func(_ kvdb.RwTx, _ *channeldb.ResolverReport) error { diff --git a/contractcourt/htlc_success_resolver.go b/contractcourt/htlc_success_resolver.go index 1770c214a4..1961f0a856 100644 --- a/contractcourt/htlc_success_resolver.go +++ b/contractcourt/htlc_success_resolver.go @@ -1,6 +1,7 @@ package contractcourt import ( + "bytes" "encoding/binary" "fmt" "io" @@ -10,6 +11,7 @@ import ( "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/graph/db/models" @@ -232,6 +234,55 @@ func (h *htlcSuccessResolver) checkpointClaim(spendTx *chainhash.Hash) error { return h.Checkpoint(h, reports...) } +// checkpointForeignSpend checkpoints the resolver as failed when the original +// HTLC outpoint was spent by a transaction that did not create our expected +// second-level success output. +func (h *htlcSuccessResolver) checkpointForeignSpend( + commitSpend *chainntnfs.SpendDetail) error { + + err := h.ChainArbitratorConfig.PutFinalHtlcOutcome( + h.ChannelArbitratorConfig.ShortChanID, h.htlc.HtlcIndex, false, + ) + if err != nil { + return err + } + + h.ChainArbitratorConfig.HtlcNotifier.NotifyFinalHtlcEvent( + models.CircuitKey{ + ChanID: h.ShortChanID, + HtlcID: h.htlc.HtlcIndex, + }, + channeldb.FinalHtlcInfo{ + Settled: false, + Offchain: false, + }, + ) + + var spendTxID *chainhash.Hash + if commitSpend != nil { + spendTxID = commitSpend.SpenderTxHash + } + h.log.Warnf("HTLC outpoint %v was spent by tx %v, which did not "+ + "create our expected second-level success output", h.outpoint(), + spendTxID) + + // A foreign spend of an incoming HTLC on our commitment means we cannot + // complete the success path. The known production case is the remote + // timeout reclaim. + report := &channeldb.ResolverReport{ + OutPoint: h.outpoint(), + Amount: h.htlc.Amt.ToSatoshis(), + ResolverType: channeldb.ResolverTypeIncomingHtlc, + ResolverOutcome: channeldb.ResolverOutcomeTimeout, + SpendTxID: spendTxID, + } + + h.outputIncubating = false + h.markResolved() + + return h.Checkpoint(h, report) +} + // Stop signals the resolver to cancel any current resolution processes, and // suspend. // @@ -431,6 +482,36 @@ func (h *htlcSuccessResolver) isTaprootFinal() bool { return h.chanType.IsTaprootFinal() } +// successTxOutpoint returns the second-level success output outpoint if the +// spending transaction created the output that this resolver expects to sweep. +func (h *htlcSuccessResolver) successTxOutpoint( + commitSpend *chainntnfs.SpendDetail) (wire.OutPoint, bool) { + + if commitSpend == nil || commitSpend.SpendingTx == nil || + commitSpend.SpenderTxHash == nil { + + return wire.OutPoint{}, false + } + + outputIndex := commitSpend.SpenderInputIndex + if outputIndex >= uint32(len(commitSpend.SpendingTx.TxOut)) { + return wire.OutPoint{}, false + } + + expected := h.htlcResolution.SweepSignDesc.Output + actual := commitSpend.SpendingTx.TxOut[outputIndex] + if expected == nil || actual.Value != expected.Value || + !bytes.Equal(actual.PkScript, expected.PkScript) { + + return wire.OutPoint{}, false + } + + return wire.OutPoint{ + Hash: *commitSpend.SpenderTxHash, + Index: outputIndex, + }, true +} + // sweepRemoteCommitOutput creates a sweep request to sweep the HTLC output on // the remote commitment via the direct preimage-spend. func (h *htlcSuccessResolver) sweepRemoteCommitOutput() error { @@ -560,6 +641,10 @@ func (h *htlcSuccessResolver) sweepSuccessTxOutput() error { if err != nil { return err } + op, ok := h.successTxOutpoint(commitSpend) + if !ok { + return h.checkpointForeignSpend(commitSpend) + } // The HTLC success tx has a CSV lock that we must wait for, and if // this is a lease enforced channel and we're the imitator, we may need @@ -583,16 +668,6 @@ func (h *htlcSuccessResolver) sweepSuccessTxOutput() error { h, h.htlc.RHash[:], waitHeight) } - // We'll use this input index to determine the second-level output - // index on the transaction, as the signatures requires the indexes to - // be the same. We don't look for the second-level output script - // directly, as there might be more than one HTLC output to the same - // pkScript. - op := &wire.OutPoint{ - Hash: *commitSpend.SpenderTxHash, - Index: commitSpend.SpenderInputIndex, - } - // Let the sweeper sweep the second-level output now that the // CSV/CLTV locks have expired. var witType input.StandardWitnessType @@ -605,7 +680,7 @@ func (h *htlcSuccessResolver) sweepSuccessTxOutput() error { witType = input.HtlcAcceptedSuccessSecondLevel } inp := h.makeSweepInput( - op, witType, + &op, witType, input.LeaseHtlcAcceptedSuccessSecondLevel, &h.htlcResolution.SweepSignDesc, h.htlcResolution.CsvDelay, uint32(commitSpend.SpendingHeight), @@ -703,15 +778,9 @@ func (h *htlcSuccessResolver) resolveSuccessTx() error { if err != nil { return err } - - // We'll use this input index to determine the second-level output - // index on the transaction, as the signatures requires the indexes to - // be the same. We don't look for the second-level output script - // directly, as there might be more than one HTLC output to the same - // pkScript. - op := wire.OutPoint{ - Hash: *commitSpend.SpenderTxHash, - Index: commitSpend.SpenderInputIndex, + op, ok := h.successTxOutpoint(commitSpend) + if !ok { + return h.checkpointForeignSpend(commitSpend) } // If the 2nd-stage sweeping has already been started, we can diff --git a/contractcourt/htlc_success_resolver_test.go b/contractcourt/htlc_success_resolver_test.go index f0f5dd8f0a..e069765a25 100644 --- a/contractcourt/htlc_success_resolver_test.go +++ b/contractcourt/htlc_success_resolver_test.go @@ -37,6 +37,7 @@ type htlcResolverTestContext struct { resolutionChan chan ResolutionMsg finalHtlcOutcomeStored bool + finalHtlcSettled bool t *testing.T } @@ -100,6 +101,7 @@ func newHtlcResolverTestContext(t *testing.T, htlcId uint64, settled bool) error { testCtx.finalHtlcOutcomeStored = true + testCtx.finalHtlcSettled = settled return nil }, @@ -333,12 +335,10 @@ func TestHtlcSuccessSecondStageResolutionSweeper(t *testing.T) { }, }, TxOut: []*wire.TxOut{ - { - Value: 123, - PkScript: []byte{0xff, 0xff}, - }, + cloneTxOut(testSignDesc.Output), }, } + successHash := successTx.TxHash() reSignedSuccessTx := &wire.MsgTx{ TxIn: []*wire.TxIn{ @@ -365,7 +365,7 @@ func TestHtlcSuccessSecondStageResolutionSweeper(t *testing.T) { successTx.TxOut[0], }, } - reSignedHash := successTx.TxHash() + reSignedHash := reSignedSuccessTx.TxHash() sweepTx := &wire.MsgTx{ TxIn: []*wire.TxIn{ @@ -400,7 +400,7 @@ func TestHtlcSuccessSecondStageResolutionSweeper(t *testing.T) { Amount: testHtlcAmt.ToSatoshis(), ResolverType: channeldb.ResolverTypeIncomingHtlc, ResolverOutcome: channeldb.ResolverOutcomeFirstStage, - SpendTxID: &reSignedHash, + SpendTxID: &successHash, } secondStage := &channeldb.ResolverReport{ @@ -534,6 +534,283 @@ func TestHtlcSuccessSecondStageResolutionSweeper(t *testing.T) { testHtlcSuccess(t, twoStageResolution, checkpoints) } +// TestHtlcSuccessResolverRejectsForeignSpend asserts that a spender which +// does not create our expected second-level success output does not get handed +// to the sweeper as a phantom input. +func TestHtlcSuccessResolverRejectsForeignSpend(t *testing.T) { + defer timeout()() + + // Arrange: set up a resolver that must first publish its HTLC success + // tx, then observe the original HTLC output being spent by a foreign + // transaction. + commitOutpoint := wire.OutPoint{ + Hash: chainhash.Hash{0x01}, + Index: 2, + } + resolution := newTestAnchorSuccessResolution(commitOutpoint) + foreignTx, foreignHash := newForeignSuccessSpend(commitOutpoint) + expectedReport := &channeldb.ResolverReport{ + OutPoint: commitOutpoint, + Amount: testHtlcAmt.ToSatoshis(), + ResolverType: channeldb.ResolverTypeIncomingHtlc, + ResolverOutcome: channeldb.ResolverOutcomeTimeout, + SpendTxID: &foreignHash, + } + ctx := newTestHtlcSuccessContext(t, resolution, false) + + checkpointChan := make(chan struct{}, 1) + ctx.checkpoint = func(resolver ContractResolver, + reports ...*channeldb.ResolverReport) error { + + successResolver, ok := resolver.(*htlcSuccessResolver) + require.True(t, ok) + + require.True(t, successResolver.IsResolved()) + require.False(t, successResolver.outputIncubating) + require.True(t, ctx.finalHtlcOutcomeStored) + require.False(t, ctx.finalHtlcSettled) + require.Equal(t, []*channeldb.ResolverReport{ + expectedReport, + }, reports) + + checkpointChan <- struct{}{} + + return nil + } + + // Act: start resolution and wait for the resolver to offer the + // first-stage HTLC output to the sweeper. + ctx.resolve() + + resolver := successResolverFromContext(t, ctx) + sweeper := mockSweeperFromResolver(t, resolver) + select { + case inp := <-sweeper.sweptInputs: + require.Equal(t, commitOutpoint, inp.OutPoint()) + case <-time.After(time.Second): + t.Fatal("expected first-stage input to be swept") + } + + // Act: deliver a foreign spend of the commitment HTLC output. + ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ + SpendingTx: foreignTx, + SpenderTxHash: &foreignHash, + SpenderInputIndex: 0, + SpendingHeight: 10, + SpentOutPoint: &commitOutpoint, + } + + select { + case <-checkpointChan: + case <-time.After(time.Second): + t.Fatal("expected foreign spend checkpoint") + } + ctx.waitForResult() + + // Assert: the resolver failed the HTLC and did not register the foreign + // transaction output as a second-stage sweep. + assertNoSweptInput(t, sweeper) + assertFinalHtlcFailed(t, ctx) +} + +// TestHtlcSuccessResolverRejectsForeignSpendOnRestart asserts that the same +// foreign-spend validation is applied after restart, when the resolver is +// already waiting for the second-level output to mature. +func TestHtlcSuccessResolverRejectsForeignSpendOnRestart(t *testing.T) { + defer timeout()() + + // Arrange: set up a restarted resolver that believes the success + // transaction is already incubating. + commitOutpoint := wire.OutPoint{ + Hash: chainhash.Hash{0x02}, + Index: 3, + } + resolution := newTestAnchorSuccessResolution(commitOutpoint) + foreignTx, foreignHash := newForeignSuccessSpend(commitOutpoint) + expectedReport := &channeldb.ResolverReport{ + OutPoint: commitOutpoint, + Amount: testHtlcAmt.ToSatoshis(), + ResolverType: channeldb.ResolverTypeIncomingHtlc, + ResolverOutcome: channeldb.ResolverOutcomeTimeout, + SpendTxID: &foreignHash, + } + ctx := newTestHtlcSuccessContext(t, resolution, true) + + checkpointChan := make(chan struct{}, 1) + ctx.checkpoint = func(resolver ContractResolver, + reports ...*channeldb.ResolverReport) error { + + successResolver, ok := resolver.(*htlcSuccessResolver) + require.True(t, ok) + + require.True(t, successResolver.IsResolved()) + require.False(t, successResolver.outputIncubating) + require.True(t, ctx.finalHtlcOutcomeStored) + require.False(t, ctx.finalHtlcSettled) + require.Equal(t, []*channeldb.ResolverReport{ + expectedReport, + }, reports) + + checkpointChan <- struct{}{} + + return nil + } + + // Act: start resolution from the incubating state and deliver a foreign + // spend of the original commitment HTLC output. + ctx.resolve() + ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ + SpendingTx: foreignTx, + SpenderTxHash: &foreignHash, + SpenderInputIndex: 0, + SpendingHeight: 10, + SpentOutPoint: &commitOutpoint, + } + + select { + case <-checkpointChan: + case <-time.After(time.Second): + t.Fatal("expected foreign spend checkpoint") + } + ctx.waitForResult() + + // Assert: the restarted resolver fails without handing the foreign + // output to the sweeper. + resolver := successResolverFromContext(t, ctx) + assertNoSweptInput(t, mockSweeperFromResolver(t, resolver)) + assertFinalHtlcFailed(t, ctx) +} + +// newTestHtlcSuccessContext creates a success resolver test context with the +// supplied incoming HTLC resolution and incubation state. +func newTestHtlcSuccessContext(t *testing.T, + resolution lnwallet.IncomingHtlcResolution, + outputIncubating bool) *htlcResolverTestContext { + + return newHtlcResolverTestContext( + t, func(htlc channeldb.HTLC, + cfg ResolverConfig) ContractResolver { + + r := newSuccessResolver( + resolution, 0, htlc, 0, cfg, + ) + r.outputIncubating = outputIncubating + + return r + }, + ) +} + +// successResolverFromContext returns the success resolver installed in a test +// context. +func successResolverFromContext(t *testing.T, + ctx *htlcResolverTestContext) *htlcSuccessResolver { + + t.Helper() + + resolver, ok := ctx.resolver.(*htlcSuccessResolver) + require.True(t, ok) + + return resolver +} + +// mockSweeperFromResolver returns the mock sweeper installed on a success +// resolver. +func mockSweeperFromResolver(t *testing.T, + resolver *htlcSuccessResolver) *mockSweeper { + + t.Helper() + + sweeper, ok := resolver.Sweeper.(*mockSweeper) + require.True(t, ok) + + return sweeper +} + +// newTestAnchorSuccessResolution returns an anchor-channel success resolution +// whose second-level output matches the shared test sign descriptor. +func newTestAnchorSuccessResolution( + commitOutpoint wire.OutPoint) lnwallet.IncomingHtlcResolution { + + successTx := &wire.MsgTx{ + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: commitOutpoint, + }, + }, + TxOut: []*wire.TxOut{ + cloneTxOut(testSignDesc.Output), + }, + } + successHash := successTx.TxHash() + + return lnwallet.IncomingHtlcResolution{ + Preimage: [32]byte{}, + CsvDelay: 4, + SignedSuccessTx: successTx, + SignDetails: &input.SignDetails{ + SignDesc: testSignDesc, + PeerSig: testSig, + }, + ClaimOutpoint: wire.OutPoint{ + Hash: successHash, + Index: 0, + }, + SweepSignDesc: testSignDesc, + } +} + +// newForeignSuccessSpend returns a transaction that spends the commitment HTLC +// outpoint without creating the expected second-level success output. +func newForeignSuccessSpend( + commitOutpoint wire.OutPoint) (*wire.MsgTx, chainhash.Hash) { + + foreignTx := &wire.MsgTx{ + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: commitOutpoint, + }, + }, + TxOut: []*wire.TxOut{ + { + Value: testSignDesc.Output.Value, + PkScript: []byte{0x51}, + }, + }, + } + + return foreignTx, foreignTx.TxHash() +} + +// cloneTxOut returns a copy of a transaction output. +func cloneTxOut(txOut *wire.TxOut) *wire.TxOut { + pkScript := append([]byte(nil), txOut.PkScript...) + return &wire.TxOut{ + Value: txOut.Value, + PkScript: pkScript, + } +} + +// assertNoSweptInput asserts that the sweeper has not received another input. +func assertNoSweptInput(t *testing.T, sweeper *mockSweeper) { + t.Helper() + + select { + case inp := <-sweeper.sweptInputs: + t.Fatalf("unexpected swept input: %v", inp.OutPoint()) + default: + } +} + +// assertFinalHtlcFailed asserts that the final HTLC outcome was persisted as a +// failure. +func assertFinalHtlcFailed(t *testing.T, ctx *htlcResolverTestContext) { + t.Helper() + + require.True(t, ctx.finalHtlcOutcomeStored) + require.False(t, ctx.finalHtlcSettled) +} + // checkpoint holds expected data we expect the resolver to checkpoint itself // to the DB next. type checkpoint struct { diff --git a/contractcourt/mock_registry_test.go b/contractcourt/mock_registry_test.go index 0530ab51dd..68317a6635 100644 --- a/contractcourt/mock_registry_test.go +++ b/contractcourt/mock_registry_test.go @@ -21,6 +21,7 @@ type mockRegistry struct { notifyChan chan notifyExitHopData notifyErr error notifyResolution invoices.HtlcResolution + immediateNotify []notifyExitHopData } func (r *mockRegistry) NotifyExitHopHtlc(payHash lntypes.Hash, @@ -31,6 +32,13 @@ func (r *mockRegistry) NotifyExitHopHtlc(payHash lntypes.Hash, // Exit early if the notification channel is nil. if hodlChan == nil { + r.immediateNotify = append(r.immediateNotify, notifyExitHopData{ + payHash: payHash, + paidAmount: paidAmount, + expiry: expiry, + currentHeight: currentHeight, + }) + return r.notifyResolution, r.notifyErr } diff --git a/docs/release-notes/release-notes-0.21.1.md b/docs/release-notes/release-notes-0.21.1.md new file mode 100644 index 0000000000..5bc2849382 --- /dev/null +++ b/docs/release-notes/release-notes-0.21.1.md @@ -0,0 +1,69 @@ +# Release Notes +- [Bug Fixes](#bug-fixes) +- [New Features](#new-features) + - [Functional Enhancements](#functional-enhancements) + - [RPC Additions](#rpc-additions) + - [lncli Additions](#lncli-additions) +- [Improvements](#improvements) + - [Functional Updates](#functional-updates) + - [RPC Updates](#rpc-updates) + - [lncli Updates](#lncli-updates) + - [Breaking Changes](#breaking-changes) + - [Performance Improvements](#performance-improvements) + - [Deprecations](#deprecations) +- [Technical and Architectural Updates](#technical-and-architectural-updates) + - [BOLT Spec Updates](#bolt-spec-updates) + - [Testing](#testing) + - [Database](#database) + - [Code Health](#code-health) + - [Robustness](#robustness) + - [Tooling and Documentation](#tooling-and-documentation) +- [Contributors (Alphabetical Order)](#contributors-alphabetical-order) + +# Bug Fixes + +* [Fixed an issue](https://github.com/lightningnetwork/lnd/issues/10840) + where an incoming HTLC resolver could treat a foreign spend of the + commitment HTLC output as its own success transaction, causing a + phantom second-level input to be offered to the sweeper. The resolver + now validates the spending transaction's expected output before + sweeping it, and passes the current chain height into launch-time + invoice-registry lookups. + +# New Features + +## Functional Enhancements + +## RPC Additions + +## lncli Additions + +# Improvements + +## Functional Updates + +## RPC Updates + +## lncli Updates + +## Breaking Changes + +## Performance Improvements + +## Deprecations + +# Technical and Architectural Updates + +## BOLT Spec Updates + +## Testing + +## Database + +## Code Health + +## Robustness + +## Tooling and Documentation + +# Contributors (Alphabetical Order)