From 6c2e8b9a00d79fd138caad79036bf992206c8505 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 30 Apr 2024 17:13:57 +0800 Subject: [PATCH 01/92] sweep: add new state `TxFatal` for erroneous sweepings Also updated the loggings. This new state will be used in the following commit. --- sweep/fee_bumper.go | 59 ++++++++++++++++++++++++++++++---------- sweep/fee_bumper_test.go | 20 ++++++++++++++ sweep/sweeper.go | 5 +++- 3 files changed, 68 insertions(+), 16 deletions(-) diff --git a/sweep/fee_bumper.go b/sweep/fee_bumper.go index 7bb58ae29e0..bfe550886f8 100644 --- a/sweep/fee_bumper.go +++ b/sweep/fee_bumper.go @@ -84,6 +84,11 @@ const ( // TxConfirmed is sent when the tx is confirmed. TxConfirmed + // TxFatal is sent when the inputs in this tx cannot be retried. Txns + // will end up in this state if they have encountered a non-fee related + // error, which means they cannot be retried with increased budget. + TxFatal + // sentinalEvent is used to check if an event is unknown. sentinalEvent ) @@ -99,6 +104,8 @@ func (e BumpEvent) String() string { return "Replaced" case TxConfirmed: return "Confirmed" + case TxFatal: + return "Fatal" default: return "Unknown" } @@ -246,10 +253,20 @@ type BumpResult struct { requestID uint64 } +// String returns a human-readable string for the result. +func (b *BumpResult) String() string { + desc := fmt.Sprintf("Event=%v", b.Event) + if b.Tx != nil { + desc += fmt.Sprintf(", Tx=%v", b.Tx.TxHash()) + } + + return fmt.Sprintf("[%s]", desc) +} + // Validate validates the BumpResult so it's safe to use. func (b *BumpResult) Validate() error { - // Every result must have a tx. - if b.Tx == nil { + // Every result must have a tx except the fatal or failed case. + if b.Tx == nil && b.Event != TxFatal { return fmt.Errorf("%w: nil tx", ErrInvalidBumpResult) } @@ -263,9 +280,11 @@ func (b *BumpResult) Validate() error { return fmt.Errorf("%w: nil replacing tx", ErrInvalidBumpResult) } - // If it's a failed event, it must have an error. - if b.Event == TxFailed && b.Err == nil { - return fmt.Errorf("%w: nil error", ErrInvalidBumpResult) + // If it's a failed or fatal event, it must have an error. + if b.Event == TxFatal || b.Event == TxFailed { + if b.Err == nil { + return fmt.Errorf("%w: nil error", ErrInvalidBumpResult) + } } // If it's a confirmed event, it must have a fee rate and fee. @@ -659,8 +678,7 @@ func (t *TxPublisher) notifyResult(result *BumpResult) { return } - log.Debugf("Sending result for requestID=%v, tx=%v", id, - result.Tx.TxHash()) + log.Debugf("Sending result %v for requestID=%v", result, id) select { // Send the result to the subscriber. @@ -678,20 +696,31 @@ func (t *TxPublisher) notifyResult(result *BumpResult) { func (t *TxPublisher) removeResult(result *BumpResult) { id := result.requestID - // Remove the record from the maps if there's an error. This means this - // tx has failed its broadcast and cannot be retried. There are two - // cases, - // - when the budget cannot cover the fee. - // - when a non-RBF related error occurs. + var txid chainhash.Hash + if result.Tx != nil { + txid = result.Tx.TxHash() + } + + // Remove the record from the maps if there's an error or the tx is + // confirmed. When there's an error, it means this tx has failed its + // broadcast and cannot be retried. There are two cases it may fail, + // - when the budget cannot cover the increased fee calculated by the + // fee function, hence the budget is used up. + // - when a non-fee related error returned from PublishTransaction. switch result.Event { case TxFailed: log.Errorf("Removing monitor record=%v, tx=%v, due to err: %v", - id, result.Tx.TxHash(), result.Err) + id, txid, result.Err) case TxConfirmed: - // Remove the record is the tx is confirmed. + // Remove the record if the tx is confirmed. log.Debugf("Removing confirmed monitor record=%v, tx=%v", id, - result.Tx.TxHash()) + txid) + + case TxFatal: + // Remove the record if there's an error. + log.Debugf("Removing monitor record=%v due to fatal err: %v", + id, result.Err) // Do nothing if it's neither failed or confirmed. default: diff --git a/sweep/fee_bumper_test.go b/sweep/fee_bumper_test.go index c9196aee5a6..0c107b29ffb 100644 --- a/sweep/fee_bumper_test.go +++ b/sweep/fee_bumper_test.go @@ -91,6 +91,19 @@ func TestBumpResultValidate(t *testing.T) { } require.ErrorIs(t, b.Validate(), ErrInvalidBumpResult) + // A failed event without a tx will give an error. + b = BumpResult{ + Event: TxFailed, + Err: errDummy, + } + require.ErrorIs(t, b.Validate(), ErrInvalidBumpResult) + + // A fatal event without a failure reason will give an error. + b = BumpResult{ + Event: TxFailed, + } + require.ErrorIs(t, b.Validate(), ErrInvalidBumpResult) + // A confirmed event without fee info will give an error. b = BumpResult{ Tx: &wire.MsgTx{}, @@ -104,6 +117,13 @@ func TestBumpResultValidate(t *testing.T) { Event: TxPublished, } require.NoError(t, b.Validate()) + + // Tx is allowed to be nil in a TxFatal event. + b = BumpResult{ + Event: TxFatal, + Err: errDummy, + } + require.NoError(t, b.Validate()) } // TestCalcSweepTxWeight checks that the weight of the sweep tx is calculated diff --git a/sweep/sweeper.go b/sweep/sweeper.go index 9eeefc94b84..a2a48beb255 100644 --- a/sweep/sweeper.go +++ b/sweep/sweeper.go @@ -1729,7 +1729,7 @@ func (s *UtxoSweeper) handleBumpEventTxPublished(r *BumpResult) error { // NOTE: TxConfirmed event is not handled, since we already subscribe to the // input's spending event, we don't need to do anything here. func (s *UtxoSweeper) handleBumpEvent(r *BumpResult) error { - log.Debugf("Received bump event [%v] for tx %v", r.Event, r.Tx.TxHash()) + log.Debugf("Received bump result %v", r) switch r.Event { // The tx has been published, we update the inputs' state and create a @@ -1745,6 +1745,9 @@ func (s *UtxoSweeper) handleBumpEvent(r *BumpResult) error { // with the new one. case TxReplaced: return s.handleBumpEventTxReplaced(r) + + case TxFatal: + // TODO(yy): create a method to remove this input. } return nil From 5f64280df4e89b1d7732272d42e45e58c52c0410 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 25 Oct 2024 15:27:26 +0800 Subject: [PATCH 02/92] sweep: add new error `ErrZeroFeeRateDelta` --- sweep/fee_function.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sweep/fee_function.go b/sweep/fee_function.go index bff44000be2..eb2ed4d6b13 100644 --- a/sweep/fee_function.go +++ b/sweep/fee_function.go @@ -14,6 +14,9 @@ var ( // ErrMaxPosition is returned when trying to increase the position of // the fee function while it's already at its max. ErrMaxPosition = errors.New("position already at max") + + // ErrZeroFeeRateDelta is returned when the fee rate delta is zero. + ErrZeroFeeRateDelta = errors.New("fee rate delta is zero") ) // mSatPerKWeight represents a fee rate in msat/kw. @@ -169,7 +172,7 @@ func NewLinearFeeFunction(maxFeeRate chainfee.SatPerKWeight, "endingFeeRate=%v, width=%v, delta=%v", start, end, l.width, l.deltaFeeRate) - return nil, fmt.Errorf("fee rate delta is zero") + return nil, ErrZeroFeeRateDelta } // Attach the calculated values to the fee function. From d0c7fd8aac0fd7476a975e8a621181655054f443 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 25 Oct 2024 15:32:30 +0800 Subject: [PATCH 03/92] sweep: add new interface method `Immediate` This prepares the following commit where we now let the fee bumpr decides whether to broadcast immediately or not. --- sweep/fee_bumper.go | 4 ++++ sweep/mock_test.go | 7 +++++++ sweep/sweeper.go | 1 + sweep/sweeper_test.go | 2 ++ sweep/tx_input_set.go | 22 ++++++++++++++++++++++ 5 files changed, 36 insertions(+) diff --git a/sweep/fee_bumper.go b/sweep/fee_bumper.go index bfe550886f8..1dff9bea88b 100644 --- a/sweep/fee_bumper.go +++ b/sweep/fee_bumper.go @@ -143,6 +143,10 @@ type BumpRequest struct { // ExtraTxOut tracks if this bump request has an optional set of extra // outputs to add to the transaction. ExtraTxOut fn.Option[SweepOutput] + + // Immediate is used to specify that the tx should be broadcast + // immediately. + Immediate bool } // MaxFeeRateAllowed returns the maximum fee rate allowed for the given diff --git a/sweep/mock_test.go b/sweep/mock_test.go index eeeb2839693..d42b0320d45 100644 --- a/sweep/mock_test.go +++ b/sweep/mock_test.go @@ -268,6 +268,13 @@ func (m *MockInputSet) StartingFeeRate() fn.Option[chainfee.SatPerKWeight] { return args.Get(0).(fn.Option[chainfee.SatPerKWeight]) } +// Immediate returns whether the inputs should be swept immediately. +func (m *MockInputSet) Immediate() bool { + args := m.Called() + + return args.Bool(0) +} + // MockBumper is a mock implementation of the interface Bumper. type MockBumper struct { mock.Mock diff --git a/sweep/sweeper.go b/sweep/sweeper.go index a2a48beb255..6245d7941bb 100644 --- a/sweep/sweeper.go +++ b/sweep/sweeper.go @@ -827,6 +827,7 @@ func (s *UtxoSweeper) sweep(set InputSet) error { DeliveryAddress: sweepAddr, MaxFeeRate: s.cfg.MaxFeeRate.FeePerKWeight(), StartingFeeRate: set.StartingFeeRate(), + Immediate: set.Immediate(), // TODO(yy): pass the strategy here. } diff --git a/sweep/sweeper_test.go b/sweep/sweeper_test.go index 7d99ba93b9a..2b527f5c886 100644 --- a/sweep/sweeper_test.go +++ b/sweep/sweeper_test.go @@ -704,11 +704,13 @@ func TestSweepPendingInputs(t *testing.T) { setNeedWallet.On("Budget").Return(btcutil.Amount(1)).Once() setNeedWallet.On("StartingFeeRate").Return( fn.None[chainfee.SatPerKWeight]()).Once() + setNeedWallet.On("Immediate").Return(false).Once() normalSet.On("Inputs").Return(nil).Maybe() normalSet.On("DeadlineHeight").Return(testHeight).Once() normalSet.On("Budget").Return(btcutil.Amount(1)).Once() normalSet.On("StartingFeeRate").Return( fn.None[chainfee.SatPerKWeight]()).Once() + normalSet.On("Immediate").Return(false).Once() // Make pending inputs for testing. We don't need real values here as // the returned clusters are mocked. diff --git a/sweep/tx_input_set.go b/sweep/tx_input_set.go index adae7cf131e..b80d52b0ea2 100644 --- a/sweep/tx_input_set.go +++ b/sweep/tx_input_set.go @@ -64,6 +64,13 @@ type InputSet interface { // StartingFeeRate returns the max starting fee rate found in the // inputs. StartingFeeRate() fn.Option[chainfee.SatPerKWeight] + + // Immediate returns a boolean to indicate whether the tx made from + // this input set should be published immediately. + // + // TODO(yy): create a new method `Params` to combine the informational + // methods DeadlineHeight, Budget, StartingFeeRate and Immediate. + Immediate() bool } // createWalletTxInput converts a wallet utxo into an object that can be added @@ -414,3 +421,18 @@ func (b *BudgetInputSet) StartingFeeRate() fn.Option[chainfee.SatPerKWeight] { return startingFeeRate } + +// Immediate returns whether the inputs should be swept immediately. +// +// NOTE: part of the InputSet interface. +func (b *BudgetInputSet) Immediate() bool { + for _, inp := range b.inputs { + // As long as one of the inputs is immediate, the whole set is + // immediate. + if inp.params.Immediate { + return true + } + } + + return false +} From 2479dc7f2eefbef7a372dfd3ec14187df2cfa902 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 25 Oct 2024 17:31:54 +0800 Subject: [PATCH 04/92] sweep: handle inputs locally instead of relying on the tx This commit changes how inputs are handled upon receiving a bump result. Previously the inputs are taken from the `BumpResult.Tx`, which is now instead being handled locally as we will remember the input set when sending the bump request, and handle this input set when a result is received. --- sweep/sweeper.go | 108 ++++++------ sweep/sweeper_test.go | 375 +++++++++++++++++++----------------------- 2 files changed, 232 insertions(+), 251 deletions(-) diff --git a/sweep/sweeper.go b/sweep/sweeper.go index 6245d7941bb..ed395480149 100644 --- a/sweep/sweeper.go +++ b/sweep/sweeper.go @@ -309,9 +309,9 @@ type UtxoSweeper struct { // updated whenever a new block epoch is received. currentHeight int32 - // bumpResultChan is a channel that receives broadcast results from the + // bumpRespChan is a channel that receives broadcast results from the // TxPublisher. - bumpResultChan chan *BumpResult + bumpRespChan chan *bumpResp } // UtxoSweeperConfig contains dependencies of UtxoSweeper. @@ -395,7 +395,7 @@ func New(cfg *UtxoSweeperConfig) *UtxoSweeper { pendingSweepsReqs: make(chan *pendingSweepsReq), quit: make(chan struct{}), inputs: make(InputsMap), - bumpResultChan: make(chan *BumpResult, 100), + bumpRespChan: make(chan *bumpResp, 100), } } @@ -681,9 +681,9 @@ func (s *UtxoSweeper) collector(blockEpochs <-chan *chainntnfs.BlockEpoch) { s.sweepPendingInputs(inputs) } - case result := <-s.bumpResultChan: + case resp := <-s.bumpRespChan: // Handle the bump event. - err := s.handleBumpEvent(result) + err := s.handleBumpEvent(resp) if err != nil { log.Errorf("Failed to handle bump event: %v", err) @@ -840,16 +840,11 @@ func (s *UtxoSweeper) sweep(set InputSet) error { // this publish result and future RBF attempt. resp, err := s.cfg.Publisher.Broadcast(req) if err != nil { - outpoints := make([]wire.OutPoint, len(set.Inputs())) - for i, inp := range set.Inputs() { - outpoints[i] = inp.OutPoint() - } - log.Errorf("Initial broadcast failed: %v, inputs=\n%v", err, inputTypeSummary(set.Inputs())) // TODO(yy): find out which input is causing the failure. - s.markInputsPublishFailed(outpoints) + s.markInputsPublishFailed(set) return err } @@ -858,7 +853,7 @@ func (s *UtxoSweeper) sweep(set InputSet) error { // subscribing to the result chan and listen for future updates about // this tx. s.wg.Add(1) - go s.monitorFeeBumpResult(resp) + go s.monitorFeeBumpResult(set, resp) return nil } @@ -868,14 +863,14 @@ func (s *UtxoSweeper) sweep(set InputSet) error { func (s *UtxoSweeper) markInputsPendingPublish(set InputSet) { // Reschedule sweep. for _, input := range set.Inputs() { - pi, ok := s.inputs[input.OutPoint()] + op := input.OutPoint() + pi, ok := s.inputs[op] if !ok { // It could be that this input is an additional wallet // input that was attached. In that case there also // isn't a pending input to update. log.Tracef("Skipped marking input as pending "+ - "published: %v not found in pending inputs", - input.OutPoint()) + "published: %v not found in pending inputs", op) continue } @@ -886,8 +881,7 @@ func (s *UtxoSweeper) markInputsPendingPublish(set InputSet) { // publish. if pi.terminated() { log.Errorf("Expect input %v to not have terminated "+ - "state, instead it has %v", - input.OutPoint, pi.state) + "state, instead it has %v", op, pi.state) continue } @@ -902,9 +896,7 @@ func (s *UtxoSweeper) markInputsPendingPublish(set InputSet) { // markInputsPublished updates the sweeping tx in db and marks the list of // inputs as published. -func (s *UtxoSweeper) markInputsPublished(tr *TxRecord, - inputs []*wire.TxIn) error { - +func (s *UtxoSweeper) markInputsPublished(tr *TxRecord, set InputSet) error { // Mark this tx in db once successfully published. // // NOTE: this will behave as an overwrite, which is fine as the record @@ -916,15 +908,15 @@ func (s *UtxoSweeper) markInputsPublished(tr *TxRecord, } // Reschedule sweep. - for _, input := range inputs { - pi, ok := s.inputs[input.PreviousOutPoint] + for _, input := range set.Inputs() { + op := input.OutPoint() + pi, ok := s.inputs[op] if !ok { // It could be that this input is an additional wallet // input that was attached. In that case there also // isn't a pending input to update. log.Tracef("Skipped marking input as published: %v "+ - "not found in pending inputs", - input.PreviousOutPoint) + "not found in pending inputs", op) continue } @@ -933,8 +925,7 @@ func (s *UtxoSweeper) markInputsPublished(tr *TxRecord, if pi.state != PendingPublish { // We may get a Published if this is a replacement tx. log.Debugf("Expect input %v to have %v, instead it "+ - "has %v", input.PreviousOutPoint, - PendingPublish, pi.state) + "has %v", op, PendingPublish, pi.state) continue } @@ -950,9 +941,10 @@ func (s *UtxoSweeper) markInputsPublished(tr *TxRecord, } // markInputsPublishFailed marks the list of inputs as failed to be published. -func (s *UtxoSweeper) markInputsPublishFailed(outpoints []wire.OutPoint) { +func (s *UtxoSweeper) markInputsPublishFailed(set InputSet) { // Reschedule sweep. - for _, op := range outpoints { + for _, inp := range set.Inputs() { + op := inp.OutPoint() pi, ok := s.inputs[op] if !ok { // It could be that this input is an additional wallet @@ -1540,6 +1532,8 @@ func (s *UtxoSweeper) updateSweeperInputs() InputsMap { // sweepPendingInputs is called when the ticker fires. It will create clusters // and attempt to create and publish the sweeping transactions. func (s *UtxoSweeper) sweepPendingInputs(inputs InputsMap) { + log.Debugf("Sweeping %v inputs", len(inputs)) + // Cluster all of our inputs based on the specific Aggregator. sets := s.cfg.Aggregator.ClusterInputs(inputs) @@ -1581,11 +1575,24 @@ func (s *UtxoSweeper) sweepPendingInputs(inputs InputsMap) { } } +// bumpResp wraps the result of a bump attempt returned from the fee bumper and +// the inputs being used. +type bumpResp struct { + // result is the result of the bump attempt returned from the fee + // bumper. + result *BumpResult + + // set is the input set that was used in the bump attempt. + set InputSet +} + // monitorFeeBumpResult subscribes to the passed result chan to listen for // future updates about the sweeping tx. // // NOTE: must run as a goroutine. -func (s *UtxoSweeper) monitorFeeBumpResult(resultChan <-chan *BumpResult) { +func (s *UtxoSweeper) monitorFeeBumpResult(set InputSet, + resultChan <-chan *BumpResult) { + defer s.wg.Done() for { @@ -1597,9 +1604,14 @@ func (s *UtxoSweeper) monitorFeeBumpResult(resultChan <-chan *BumpResult) { continue } + resp := &bumpResp{ + result: r, + set: set, + } + // Send the result back to the main event loop. select { - case s.bumpResultChan <- r: + case s.bumpRespChan <- resp: case <-s.quit: log.Debug("Sweeper shutting down, skip " + "sending bump result") @@ -1635,25 +1647,25 @@ func (s *UtxoSweeper) monitorFeeBumpResult(resultChan <-chan *BumpResult) { // handleBumpEventTxFailed handles the case where the tx has been failed to // publish. -func (s *UtxoSweeper) handleBumpEventTxFailed(r *BumpResult) error { +func (s *UtxoSweeper) handleBumpEventTxFailed(resp *bumpResp) { + r := resp.result tx, err := r.Tx, r.Err log.Errorf("Fee bump attempt failed for tx=%v: %v", tx.TxHash(), err) - outpoints := make([]wire.OutPoint, 0, len(tx.TxIn)) - for _, inp := range tx.TxIn { - outpoints = append(outpoints, inp.PreviousOutPoint) - } - + // NOTE: When marking the inputs as failed, we are using the input set + // instead of the inputs found in the tx. This is fine for current + // version of the sweeper because we always create a tx using ALL of + // the inputs specified by the set. + // // TODO(yy): should we also remove the failed tx from db? - s.markInputsPublishFailed(outpoints) - - return err + s.markInputsPublishFailed(resp.set) } // handleBumpEventTxReplaced handles the case where the sweeping tx has been // replaced by a new one. -func (s *UtxoSweeper) handleBumpEventTxReplaced(r *BumpResult) error { +func (s *UtxoSweeper) handleBumpEventTxReplaced(resp *bumpResp) error { + r := resp.result oldTx := r.ReplacedTx newTx := r.Tx @@ -1693,12 +1705,13 @@ func (s *UtxoSweeper) handleBumpEventTxReplaced(r *BumpResult) error { } // Mark the inputs as published using the replacing tx. - return s.markInputsPublished(tr, r.Tx.TxIn) + return s.markInputsPublished(tr, resp.set) } // handleBumpEventTxPublished handles the case where the sweeping tx has been // successfully published. -func (s *UtxoSweeper) handleBumpEventTxPublished(r *BumpResult) error { +func (s *UtxoSweeper) handleBumpEventTxPublished(resp *bumpResp) error { + r := resp.result tx := r.Tx tr := &TxRecord{ Txid: tx.TxHash(), @@ -1708,7 +1721,7 @@ func (s *UtxoSweeper) handleBumpEventTxPublished(r *BumpResult) error { // Inputs have been successfully published so we update their // states. - err := s.markInputsPublished(tr, tx.TxIn) + err := s.markInputsPublished(tr, resp.set) if err != nil { return err } @@ -1729,10 +1742,10 @@ func (s *UtxoSweeper) handleBumpEventTxPublished(r *BumpResult) error { // // NOTE: TxConfirmed event is not handled, since we already subscribe to the // input's spending event, we don't need to do anything here. -func (s *UtxoSweeper) handleBumpEvent(r *BumpResult) error { - log.Debugf("Received bump result %v", r) +func (s *UtxoSweeper) handleBumpEvent(r *bumpResp) error { + log.Debugf("Received bump result %v", r.result) - switch r.Event { + switch r.result.Event { // The tx has been published, we update the inputs' state and create a // record to be stored in the sweeper db. case TxPublished: @@ -1740,7 +1753,8 @@ func (s *UtxoSweeper) handleBumpEvent(r *BumpResult) error { // The tx has failed, we update the inputs' state. case TxFailed: - return s.handleBumpEventTxFailed(r) + s.handleBumpEventTxFailed(r) + return nil // The tx has been replaced, we will remove the old tx and replace it // with the new one. diff --git a/sweep/sweeper_test.go b/sweep/sweeper_test.go index 2b527f5c886..2e1be71c264 100644 --- a/sweep/sweeper_test.go +++ b/sweep/sweeper_test.go @@ -1,6 +1,7 @@ package sweep import ( + "crypto/rand" "errors" "testing" "time" @@ -12,6 +13,7 @@ import ( "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/stretchr/testify/mock" @@ -33,6 +35,41 @@ var ( }) ) +// createMockInput creates a mock input and saves it to the sweeper's inputs +// map. The created input has the specified state and a random outpoint. It +// will assert the method `OutPoint` is called at least once. +func createMockInput(t *testing.T, s *UtxoSweeper, + state SweepState) *input.MockInput { + + inp := &input.MockInput{} + t.Cleanup(func() { + inp.AssertExpectations(t) + }) + + randBuf := make([]byte, lntypes.HashSize) + _, err := rand.Read(randBuf) + require.NoError(t, err, "internal error, cannot generate random bytes") + + randHash, err := chainhash.NewHash(randBuf) + require.NoError(t, err) + + inp.On("OutPoint").Return(wire.OutPoint{ + Hash: *randHash, + Index: 0, + }) + + // We don't do branch switches based on the witness type here so we + // just mock it. + inp.On("WitnessType").Return(input.CommitmentTimeLock).Maybe() + + s.inputs[inp.OutPoint()] = &SweeperInput{ + Input: inp, + state: state, + } + + return inp +} + // TestMarkInputsPendingPublish checks that given a list of inputs with // different states, only the non-terminal state will be marked as `Published`. func TestMarkInputsPendingPublish(t *testing.T) { @@ -47,50 +84,21 @@ func TestMarkInputsPendingPublish(t *testing.T) { set := &MockInputSet{} defer set.AssertExpectations(t) - // Create three testing inputs. - // - // inputNotExist specifies an input that's not found in the sweeper's - // `pendingInputs` map. - inputNotExist := &input.MockInput{} - defer inputNotExist.AssertExpectations(t) - - inputNotExist.On("OutPoint").Return(wire.OutPoint{Index: 0}) - - // inputInit specifies a newly created input. - inputInit := &input.MockInput{} - defer inputInit.AssertExpectations(t) - - inputInit.On("OutPoint").Return(wire.OutPoint{Index: 1}) - - s.inputs[inputInit.OutPoint()] = &SweeperInput{ - state: Init, - } - - // inputPendingPublish specifies an input that's about to be published. - inputPendingPublish := &input.MockInput{} - defer inputPendingPublish.AssertExpectations(t) - - inputPendingPublish.On("OutPoint").Return(wire.OutPoint{Index: 2}) - - s.inputs[inputPendingPublish.OutPoint()] = &SweeperInput{ - state: PendingPublish, - } - - // inputTerminated specifies an input that's terminated. - inputTerminated := &input.MockInput{} - defer inputTerminated.AssertExpectations(t) - - inputTerminated.On("OutPoint").Return(wire.OutPoint{Index: 3}) - - s.inputs[inputTerminated.OutPoint()] = &SweeperInput{ - state: Excluded, - } + // Create three inputs with different states. + // - inputInit specifies a newly created input. + // - inputPendingPublish specifies an input about to be published. + // - inputTerminated specifies an input that's terminated. + var ( + inputInit = createMockInput(t, s, Init) + inputPendingPublish = createMockInput(t, s, PendingPublish) + inputTerminated = createMockInput(t, s, Excluded) + ) // Mark the test inputs. We expect the non-exist input and the // inputTerminated to be skipped, and the rest to be marked as pending // publish. set.On("Inputs").Return([]input.Input{ - inputNotExist, inputInit, inputPendingPublish, inputTerminated, + inputInit, inputPendingPublish, inputTerminated, }) s.markInputsPendingPublish(set) @@ -122,36 +130,22 @@ func TestMarkInputsPublished(t *testing.T) { dummyTR := &TxRecord{} dummyErr := errors.New("dummy error") + // Create a mock input set. + set := &MockInputSet{} + defer set.AssertExpectations(t) + // Create a test sweeper. s := New(&UtxoSweeperConfig{ Store: mockStore, }) - // Create three testing inputs. - // - // inputNotExist specifies an input that's not found in the sweeper's - // `inputs` map. - inputNotExist := &wire.TxIn{ - PreviousOutPoint: wire.OutPoint{Index: 1}, - } - - // inputInit specifies a newly created input. When marking this as - // published, we should see an error log as this input hasn't been - // published yet. - inputInit := &wire.TxIn{ - PreviousOutPoint: wire.OutPoint{Index: 2}, - } - s.inputs[inputInit.PreviousOutPoint] = &SweeperInput{ - state: Init, - } - - // inputPendingPublish specifies an input that's about to be published. - inputPendingPublish := &wire.TxIn{ - PreviousOutPoint: wire.OutPoint{Index: 3}, - } - s.inputs[inputPendingPublish.PreviousOutPoint] = &SweeperInput{ - state: PendingPublish, - } + // Create two inputs with different states. + // - inputInit specifies a newly created input. + // - inputPendingPublish specifies an input about to be published. + var ( + inputInit = createMockInput(t, s, Init) + inputPendingPublish = createMockInput(t, s, PendingPublish) + ) // First, check that when an error is returned from db, it's properly // returned here. @@ -171,9 +165,9 @@ func TestMarkInputsPublished(t *testing.T) { // Mark the test inputs. We expect the non-exist input and the // inputInit to be skipped, and the final input to be marked as // published. - err = s.markInputsPublished(dummyTR, []*wire.TxIn{ - inputNotExist, inputInit, inputPendingPublish, - }) + set.On("Inputs").Return([]input.Input{inputInit, inputPendingPublish}) + + err = s.markInputsPublished(dummyTR, set) require.NoError(err) // We expect unchanged number of pending inputs. @@ -181,11 +175,11 @@ func TestMarkInputsPublished(t *testing.T) { // We expect the init input's state to stay unchanged. require.Equal(Init, - s.inputs[inputInit.PreviousOutPoint].state) + s.inputs[inputInit.OutPoint()].state) // We expect the pending-publish input's is now marked as published. require.Equal(Published, - s.inputs[inputPendingPublish.PreviousOutPoint].state) + s.inputs[inputPendingPublish.OutPoint()].state) // Assert mocked statements are executed as expected. mockStore.AssertExpectations(t) @@ -202,117 +196,75 @@ func TestMarkInputsPublishFailed(t *testing.T) { // Create a mock sweeper store. mockStore := NewMockSweeperStore() + // Create a mock input set. + set := &MockInputSet{} + defer set.AssertExpectations(t) + // Create a test sweeper. s := New(&UtxoSweeperConfig{ Store: mockStore, }) - // Create testing inputs for each state. - // - // inputNotExist specifies an input that's not found in the sweeper's - // `inputs` map. - inputNotExist := &wire.TxIn{ - PreviousOutPoint: wire.OutPoint{Index: 1}, - } - - // inputInit specifies a newly created input. When marking this as - // published, we should see an error log as this input hasn't been - // published yet. - inputInit := &wire.TxIn{ - PreviousOutPoint: wire.OutPoint{Index: 2}, - } - s.inputs[inputInit.PreviousOutPoint] = &SweeperInput{ - state: Init, - } - - // inputPendingPublish specifies an input that's about to be published. - inputPendingPublish := &wire.TxIn{ - PreviousOutPoint: wire.OutPoint{Index: 3}, - } - s.inputs[inputPendingPublish.PreviousOutPoint] = &SweeperInput{ - state: PendingPublish, - } - - // inputPublished specifies an input that's published. - inputPublished := &wire.TxIn{ - PreviousOutPoint: wire.OutPoint{Index: 4}, - } - s.inputs[inputPublished.PreviousOutPoint] = &SweeperInput{ - state: Published, - } - - // inputPublishFailed specifies an input that's failed to be published. - inputPublishFailed := &wire.TxIn{ - PreviousOutPoint: wire.OutPoint{Index: 5}, - } - s.inputs[inputPublishFailed.PreviousOutPoint] = &SweeperInput{ - state: PublishFailed, - } - - // inputSwept specifies an input that's swept. - inputSwept := &wire.TxIn{ - PreviousOutPoint: wire.OutPoint{Index: 6}, - } - s.inputs[inputSwept.PreviousOutPoint] = &SweeperInput{ - state: Swept, - } - - // inputExcluded specifies an input that's excluded. - inputExcluded := &wire.TxIn{ - PreviousOutPoint: wire.OutPoint{Index: 7}, - } - s.inputs[inputExcluded.PreviousOutPoint] = &SweeperInput{ - state: Excluded, - } - - // inputFailed specifies an input that's failed. - inputFailed := &wire.TxIn{ - PreviousOutPoint: wire.OutPoint{Index: 8}, - } - s.inputs[inputFailed.PreviousOutPoint] = &SweeperInput{ - state: Failed, - } + // Create inputs with different states. + // - inputInit specifies a newly created input. When marking this as + // published, we should see an error log as this input hasn't been + // published yet. + // - inputPendingPublish specifies an input about to be published. + // - inputPublished specifies an input that's published. + // - inputPublishFailed specifies an input that's failed to be + // published. + // - inputSwept specifies an input that's swept. + // - inputExcluded specifies an input that's excluded. + // - inputFailed specifies an input that's failed. + var ( + inputInit = createMockInput(t, s, Init) + inputPendingPublish = createMockInput(t, s, PendingPublish) + inputPublished = createMockInput(t, s, Published) + inputPublishFailed = createMockInput(t, s, PublishFailed) + inputSwept = createMockInput(t, s, Swept) + inputExcluded = createMockInput(t, s, Excluded) + inputFailed = createMockInput(t, s, Failed) + ) - // Gather all inputs' outpoints. - pendingOps := make([]wire.OutPoint, 0, len(s.inputs)+1) - for op := range s.inputs { - pendingOps = append(pendingOps, op) - } - pendingOps = append(pendingOps, inputNotExist.PreviousOutPoint) + // Gather all inputs. + set.On("Inputs").Return([]input.Input{ + inputInit, inputPendingPublish, inputPublished, + inputPublishFailed, inputSwept, inputExcluded, inputFailed, + }) // Mark the test inputs. We expect the non-exist input and the // inputInit to be skipped, and the final input to be marked as // published. - s.markInputsPublishFailed(pendingOps) + s.markInputsPublishFailed(set) // We expect unchanged number of pending inputs. require.Len(s.inputs, 7) // We expect the init input's state to stay unchanged. require.Equal(Init, - s.inputs[inputInit.PreviousOutPoint].state) + s.inputs[inputInit.OutPoint()].state) // We expect the pending-publish input's is now marked as publish // failed. require.Equal(PublishFailed, - s.inputs[inputPendingPublish.PreviousOutPoint].state) + s.inputs[inputPendingPublish.OutPoint()].state) // We expect the published input's is now marked as publish failed. require.Equal(PublishFailed, - s.inputs[inputPublished.PreviousOutPoint].state) + s.inputs[inputPublished.OutPoint()].state) // We expect the publish failed input to stay unchanged. require.Equal(PublishFailed, - s.inputs[inputPublishFailed.PreviousOutPoint].state) + s.inputs[inputPublishFailed.OutPoint()].state) // We expect the swept input to stay unchanged. - require.Equal(Swept, s.inputs[inputSwept.PreviousOutPoint].state) + require.Equal(Swept, s.inputs[inputSwept.OutPoint()].state) // We expect the excluded input to stay unchanged. - require.Equal(Excluded, s.inputs[inputExcluded.PreviousOutPoint].state) + require.Equal(Excluded, s.inputs[inputExcluded.OutPoint()].state) // We expect the failed input to stay unchanged. - require.Equal(Failed, s.inputs[inputFailed.PreviousOutPoint].state) + require.Equal(Failed, s.inputs[inputFailed.OutPoint()].state) // Assert mocked statements are executed as expected. mockStore.AssertExpectations(t) @@ -738,33 +690,33 @@ func TestSweepPendingInputs(t *testing.T) { func TestHandleBumpEventTxFailed(t *testing.T) { t.Parallel() + // Create a mock input set. + set := &MockInputSet{} + defer set.AssertExpectations(t) + // Create a test sweeper. s := New(&UtxoSweeperConfig{}) - var ( - // Create four testing outpoints. - op1 = wire.OutPoint{Hash: chainhash.Hash{1}} - op2 = wire.OutPoint{Hash: chainhash.Hash{2}} - op3 = wire.OutPoint{Hash: chainhash.Hash{3}} - opNotExist = wire.OutPoint{Hash: chainhash.Hash{4}} - ) + // inputNotExist specifies an input that's not found in the sweeper's + // `pendingInputs` map. + inputNotExist := &input.MockInput{} + defer inputNotExist.AssertExpectations(t) + inputNotExist.On("OutPoint").Return(wire.OutPoint{Index: 0}) + opNotExist := inputNotExist.OutPoint() // Create three mock inputs. - input1 := &input.MockInput{} - defer input1.AssertExpectations(t) - - input2 := &input.MockInput{} - defer input2.AssertExpectations(t) + var ( + input1 = createMockInput(t, s, PendingPublish) + input2 = createMockInput(t, s, PendingPublish) + input3 = createMockInput(t, s, PendingPublish) + ) - input3 := &input.MockInput{} - defer input3.AssertExpectations(t) + op1 := input1.OutPoint() + op2 := input2.OutPoint() + op3 := input3.OutPoint() // Construct the initial state for the sweeper. - s.inputs = InputsMap{ - op1: &SweeperInput{Input: input1, state: PendingPublish}, - op2: &SweeperInput{Input: input2, state: PendingPublish}, - op3: &SweeperInput{Input: input3, state: PendingPublish}, - } + set.On("Inputs").Return([]input.Input{input1, input2, input3}) // Create a testing tx that spends the first two inputs. tx := &wire.MsgTx{ @@ -782,16 +734,26 @@ func TestHandleBumpEventTxFailed(t *testing.T) { Err: errDummy, } + // Create a testing bump response. + resp := &bumpResp{ + result: br, + set: set, + } + // Call the method under test. - err := s.handleBumpEvent(br) + err := s.handleBumpEvent(resp) require.ErrorIs(t, err, errDummy) // Assert the states of the first two inputs are updated. require.Equal(t, PublishFailed, s.inputs[op1].state) require.Equal(t, PublishFailed, s.inputs[op2].state) - // Assert the state of the third input is not updated. - require.Equal(t, PendingPublish, s.inputs[op3].state) + // Assert the state of the third input. + // + // NOTE: Although the tx doesn't spend it, we still mark this input as + // failed as we are treating the input set as the single source of + // truth. + require.Equal(t, PublishFailed, s.inputs[op3].state) // Assert the non-existing input is not added to the pending inputs. require.NotContains(t, s.inputs, opNotExist) @@ -810,23 +772,21 @@ func TestHandleBumpEventTxReplaced(t *testing.T) { wallet := &MockWallet{} defer wallet.AssertExpectations(t) + // Create a mock input set. + set := &MockInputSet{} + defer set.AssertExpectations(t) + // Create a test sweeper. s := New(&UtxoSweeperConfig{ Store: store, Wallet: wallet, }) - // Create a testing outpoint. - op := wire.OutPoint{Hash: chainhash.Hash{1}} - // Create a mock input. - inp := &input.MockInput{} - defer inp.AssertExpectations(t) + inp := createMockInput(t, s, PendingPublish) + set.On("Inputs").Return([]input.Input{inp}) - // Construct the initial state for the sweeper. - s.inputs = InputsMap{ - op: &SweeperInput{Input: inp, state: PendingPublish}, - } + op := inp.OutPoint() // Create a testing tx that spends the input. tx := &wire.MsgTx{ @@ -851,12 +811,18 @@ func TestHandleBumpEventTxReplaced(t *testing.T) { Event: TxReplaced, } + // Create a testing bump response. + resp := &bumpResp{ + result: br, + set: set, + } + // Mock the store to return an error. dummyErr := errors.New("dummy error") store.On("GetTx", tx.TxHash()).Return(nil, dummyErr).Once() // Call the method under test and assert the error is returned. - err := s.handleBumpEventTxReplaced(br) + err := s.handleBumpEventTxReplaced(resp) require.ErrorIs(t, err, dummyErr) // Mock the store to return the old tx record. @@ -871,7 +837,7 @@ func TestHandleBumpEventTxReplaced(t *testing.T) { store.On("DeleteTx", tx.TxHash()).Return(dummyErr).Once() // Call the method under test and assert the error is returned. - err = s.handleBumpEventTxReplaced(br) + err = s.handleBumpEventTxReplaced(resp) require.ErrorIs(t, err, dummyErr) // Mock the store to return the old tx record and delete it without @@ -891,7 +857,7 @@ func TestHandleBumpEventTxReplaced(t *testing.T) { wallet.On("CancelRebroadcast", tx.TxHash()).Once() // Call the method under test. - err = s.handleBumpEventTxReplaced(br) + err = s.handleBumpEventTxReplaced(resp) require.NoError(t, err) // Assert the state of the input is updated. @@ -907,22 +873,20 @@ func TestHandleBumpEventTxPublished(t *testing.T) { store := &MockSweeperStore{} defer store.AssertExpectations(t) + // Create a mock input set. + set := &MockInputSet{} + defer set.AssertExpectations(t) + // Create a test sweeper. s := New(&UtxoSweeperConfig{ Store: store, }) - // Create a testing outpoint. - op := wire.OutPoint{Hash: chainhash.Hash{1}} - // Create a mock input. - inp := &input.MockInput{} - defer inp.AssertExpectations(t) + inp := createMockInput(t, s, PendingPublish) + set.On("Inputs").Return([]input.Input{inp}) - // Construct the initial state for the sweeper. - s.inputs = InputsMap{ - op: &SweeperInput{Input: inp, state: PendingPublish}, - } + op := inp.OutPoint() // Create a testing tx that spends the input. tx := &wire.MsgTx{ @@ -938,6 +902,12 @@ func TestHandleBumpEventTxPublished(t *testing.T) { Event: TxPublished, } + // Create a testing bump response. + resp := &bumpResp{ + result: br, + set: set, + } + // Mock the store to save the new tx record. store.On("StoreTx", &TxRecord{ Txid: tx.TxHash(), @@ -945,7 +915,7 @@ func TestHandleBumpEventTxPublished(t *testing.T) { }).Return(nil).Once() // Call the method under test. - err := s.handleBumpEventTxPublished(br) + err := s.handleBumpEventTxPublished(resp) require.NoError(t, err) // Assert the state of the input is updated. @@ -963,25 +933,21 @@ func TestMonitorFeeBumpResult(t *testing.T) { wallet := &MockWallet{} defer wallet.AssertExpectations(t) + // Create a mock input set. + set := &MockInputSet{} + defer set.AssertExpectations(t) + // Create a test sweeper. s := New(&UtxoSweeperConfig{ Store: store, Wallet: wallet, }) - // Create a testing outpoint. - op := wire.OutPoint{Hash: chainhash.Hash{1}} - // Create a mock input. - inp := &input.MockInput{} - defer inp.AssertExpectations(t) - - // Construct the initial state for the sweeper. - s.inputs = InputsMap{ - op: &SweeperInput{Input: inp, state: PendingPublish}, - } + inp := createMockInput(t, s, PendingPublish) // Create a testing tx that spends the input. + op := inp.OutPoint() tx := &wire.MsgTx{ LockTime: 1, TxIn: []*wire.TxIn{ @@ -1060,7 +1026,8 @@ func TestMonitorFeeBumpResult(t *testing.T) { return resultChan }, shouldExit: false, - }, { + }, + { // When the sweeper is shutting down, the monitor loop // should exit. name: "exit on sweeper shutdown", @@ -1087,7 +1054,7 @@ func TestMonitorFeeBumpResult(t *testing.T) { s.wg.Add(1) go func() { - s.monitorFeeBumpResult(resultChan) + s.monitorFeeBumpResult(set, resultChan) close(done) }() From 77ff2c0585c481e73cbefec97a7c93d6b2065aee Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 25 Oct 2024 18:31:46 +0800 Subject: [PATCH 05/92] sweep: add `handleInitialBroadcast` to handle initial broadcast This commit adds a new method `handleInitialBroadcast` to handle the initial broadcast. Previously we'd broadcast immediately inside `Broadcast`, which soon will not work after the `blockbeat` is implemented as the action to publish is now always triggered by a new block. Meanwhile, we still keep the option to bypass the block trigger so users can broadcast immediately by setting `Immediate` to true. --- sweep/fee_bumper.go | 172 ++++++++++++++----- sweep/fee_bumper_test.go | 362 ++++++++++++++++++++++++++------------- 2 files changed, 370 insertions(+), 164 deletions(-) diff --git a/sweep/fee_bumper.go b/sweep/fee_bumper.go index 1dff9bea88b..1e159fa1277 100644 --- a/sweep/fee_bumper.go +++ b/sweep/fee_bumper.go @@ -376,40 +376,52 @@ func (t *TxPublisher) isNeutrinoBackend() bool { return t.cfg.Wallet.BackEnd() == "neutrino" } -// Broadcast is used to publish the tx created from the given inputs. It will, -// 1. init a fee function based on the given strategy. -// 2. create an RBF-compliant tx and monitor it for confirmation. -// 3. notify the initial broadcast result back to the caller. -// The initial broadcast is guaranteed to be RBF-compliant unless the budget -// specified cannot cover the fee. +// Broadcast is used to publish the tx created from the given inputs. It will +// register the broadcast request and return a chan to the caller to subscribe +// the broadcast result. The initial broadcast is guaranteed to be +// RBF-compliant unless the budget specified cannot cover the fee. // // NOTE: part of the Bumper interface. func (t *TxPublisher) Broadcast(req *BumpRequest) (<-chan *BumpResult, error) { log.Tracef("Received broadcast request: %s", lnutils.SpewLogClosure( req)) - // Attempt an initial broadcast which is guaranteed to comply with the - // RBF rules. - result, err := t.initialBroadcast(req) - if err != nil { - log.Errorf("Initial broadcast failed: %v", err) - - return nil, err - } + // Store the request. + requestID, record := t.storeInitialRecord(req) // Create a chan to send the result to the caller. subscriber := make(chan *BumpResult, 1) - t.subscriberChans.Store(result.requestID, subscriber) + t.subscriberChans.Store(requestID, subscriber) - // Send the initial broadcast result to the caller. - t.handleResult(result) + // Publish the tx immediately if specified. + if req.Immediate { + t.handleInitialBroadcast(record, requestID) + } return subscriber, nil } +// storeInitialRecord initializes a monitor record and saves it in the map. +func (t *TxPublisher) storeInitialRecord(req *BumpRequest) ( + uint64, *monitorRecord) { + + // Increase the request counter. + // + // NOTE: this is the only place where we increase the counter. + requestID := t.requestCounter.Add(1) + + // Register the record. + record := &monitorRecord{req: req} + t.records.Store(requestID, record) + + return requestID, record +} + // initialBroadcast initializes a fee function, creates an RBF-compliant tx and // broadcasts it. -func (t *TxPublisher) initialBroadcast(req *BumpRequest) (*BumpResult, error) { +func (t *TxPublisher) initialBroadcast(requestID uint64, + req *BumpRequest) (*BumpResult, error) { + // Create a fee bumping algorithm to be used for future RBF. feeAlgo, err := t.initializeFeeFunction(req) if err != nil { @@ -418,7 +430,7 @@ func (t *TxPublisher) initialBroadcast(req *BumpRequest) (*BumpResult, error) { // Create the initial tx to be broadcasted. This tx is guaranteed to // comply with the RBF restrictions. - requestID, err := t.createRBFCompliantTx(req, feeAlgo) + err = t.createRBFCompliantTx(requestID, req, feeAlgo) if err != nil { return nil, fmt.Errorf("create RBF-compliant tx: %w", err) } @@ -465,8 +477,8 @@ func (t *TxPublisher) initializeFeeFunction( // so by creating a tx, validate it using `TestMempoolAccept`, and bump its fee // and redo the process until the tx is valid, or return an error when non-RBF // related errors occur or the budget has been used up. -func (t *TxPublisher) createRBFCompliantTx(req *BumpRequest, - f FeeFunction) (uint64, error) { +func (t *TxPublisher) createRBFCompliantTx(requestID uint64, req *BumpRequest, + f FeeFunction) error { for { // Create a new tx with the given fee rate and check its @@ -475,18 +487,19 @@ func (t *TxPublisher) createRBFCompliantTx(req *BumpRequest, switch { case err == nil: - // The tx is valid, return the request ID. - requestID := t.storeRecord( - sweepCtx.tx, req, f, sweepCtx.fee, + // The tx is valid, store it. + t.storeRecord( + requestID, sweepCtx.tx, req, f, sweepCtx.fee, sweepCtx.outpointToTxIndex, ) - log.Infof("Created tx %v for %v inputs: feerate=%v, "+ - "fee=%v, inputs=%v", sweepCtx.tx.TxHash(), - len(req.Inputs), f.FeeRate(), sweepCtx.fee, + log.Infof("Created initial sweep tx=%v for %v inputs: "+ + "feerate=%v, fee=%v, inputs:\n%v", + sweepCtx.tx.TxHash(), len(req.Inputs), + f.FeeRate(), sweepCtx.fee, inputTypeSummary(req.Inputs)) - return requestID, nil + return nil // If the error indicates the fees paid is not enough, we will // ask the fee function to increase the fee rate and retry. @@ -517,7 +530,7 @@ func (t *TxPublisher) createRBFCompliantTx(req *BumpRequest, // cluster these inputs differetly. increased, err = f.Increment() if err != nil { - return 0, err + return err } } @@ -527,21 +540,15 @@ func (t *TxPublisher) createRBFCompliantTx(req *BumpRequest, // mempool acceptance. default: log.Debugf("Failed to create RBF-compliant tx: %v", err) - return 0, err + return err } } } // storeRecord stores the given record in the records map. -func (t *TxPublisher) storeRecord(tx *wire.MsgTx, req *BumpRequest, - f FeeFunction, fee btcutil.Amount, - outpointToTxIndex map[wire.OutPoint]int) uint64 { - - // Increase the request counter. - // - // NOTE: this is the only place where we increase the - // counter. - requestID := t.requestCounter.Add(1) +func (t *TxPublisher) storeRecord(requestID uint64, tx *wire.MsgTx, + req *BumpRequest, f FeeFunction, fee btcutil.Amount, + outpointToTxIndex map[wire.OutPoint]int) { // Register the record. t.records.Store(requestID, &monitorRecord{ @@ -551,8 +558,6 @@ func (t *TxPublisher) storeRecord(tx *wire.MsgTx, req *BumpRequest, fee: fee, outpointToTxIndex: outpointToTxIndex, }) - - return requestID } // createAndCheckTx creates a tx based on the given inputs, change output @@ -852,18 +857,27 @@ func (t *TxPublisher) processRecords() { // confirmed. confirmedRecords := make(map[uint64]*monitorRecord) - // feeBumpRecords stores a map of the records which need to be bumped. + // feeBumpRecords stores a map of records which need to be bumped. feeBumpRecords := make(map[uint64]*monitorRecord) - // failedRecords stores a map of the records which has inputs being - // spent by a third party. + // failedRecords stores a map of records which has inputs being spent + // by a third party. // // NOTE: this is only used for neutrino backend. failedRecords := make(map[uint64]*monitorRecord) + // initialRecords stores a map of records which are being created and + // published for the first time. + initialRecords := make(map[uint64]*monitorRecord) + // visitor is a helper closure that visits each record and divides them // into two groups. visitor := func(requestID uint64, r *monitorRecord) error { + if r.tx == nil { + initialRecords[requestID] = r + return nil + } + log.Tracef("Checking monitor recordID=%v for tx=%v", requestID, r.tx.TxHash()) @@ -891,9 +905,14 @@ func (t *TxPublisher) processRecords() { return nil } - // Iterate through all the records and divide them into two groups. + // Iterate through all the records and divide them into four groups. t.records.ForEach(visitor) + // Handle the initial broadcast. + for requestID, r := range initialRecords { + t.handleInitialBroadcast(r, requestID) + } + // For records that are confirmed, we'll notify the caller about this // result. for requestID, r := range confirmedRecords { @@ -949,6 +968,69 @@ func (t *TxPublisher) handleTxConfirmed(r *monitorRecord, requestID uint64) { t.handleResult(result) } +// handleInitialBroadcast is called when a new request is received. It will +// handle the initial tx creation and broadcast. In details, +// 1. init a fee function based on the given strategy. +// 2. create an RBF-compliant tx and monitor it for confirmation. +// 3. notify the initial broadcast result back to the caller. +func (t *TxPublisher) handleInitialBroadcast(r *monitorRecord, + requestID uint64) { + + log.Debugf("Initial broadcast for requestID=%v", requestID) + + var ( + result *BumpResult + err error + ) + + // Attempt an initial broadcast which is guaranteed to comply with the + // RBF rules. + result, err = t.initialBroadcast(requestID, r.req) + if err != nil { + log.Errorf("Initial broadcast failed: %v", err) + + // We now decide what type of event to send. + var event BumpEvent + + switch { + // When the error is due to a dust output, we'll send a + // TxFailed so these inputs can be retried with a different + // group in the next block. + case errors.Is(err, ErrTxNoOutput): + event = TxFailed + + // When the error is due to budget being used up, we'll send a + // TxFailed so these inputs can be retried with a different + // group in the next block. + case errors.Is(err, ErrMaxPosition): + event = TxFailed + + // When the error is due to zero fee rate delta, we'll send a + // TxFailed so these inputs can be retried in the next block. + case errors.Is(err, ErrZeroFeeRateDelta): + event = TxFailed + + // Otherwise this is not a fee-related error and the tx cannot + // be retried. In that case we will fail ALL the inputs in this + // tx, which means they will be removed from the sweeper and + // never be tried again. + // + // TODO(yy): Find out which input is causing the failure and + // fail that one only. + default: + event = TxFatal + } + + result = &BumpResult{ + Event: event, + Err: err, + requestID: requestID, + } + } + + t.handleResult(result) +} + // handleFeeBumpTx checks if the tx needs to be bumped, and if so, it will // attempt to bump the fee of the tx. // diff --git a/sweep/fee_bumper_test.go b/sweep/fee_bumper_test.go index 0c107b29ffb..50c1fdced60 100644 --- a/sweep/fee_bumper_test.go +++ b/sweep/fee_bumper_test.go @@ -352,13 +352,10 @@ func TestStoreRecord(t *testing.T) { } // Call the method under test. - requestID := tp.storeRecord(tx, req, feeFunc, fee, utxoIndex) - - // Check the request ID is as expected. - require.Equal(t, initialCounter+1, requestID) + tp.storeRecord(initialCounter, tx, req, feeFunc, fee, utxoIndex) // Read the saved record and compare. - record, ok := tp.records.Load(requestID) + record, ok := tp.records.Load(initialCounter) require.True(t, ok) require.Equal(t, tx, record.tx) require.Equal(t, feeFunc, record.feeFunction) @@ -655,23 +652,19 @@ func TestCreateRBFCompliantTx(t *testing.T) { }, } + var requestCounter atomic.Uint64 for _, tc := range testCases { tc := tc + rid := requestCounter.Add(1) t.Run(tc.name, func(t *testing.T) { tc.setupMock() // Call the method under test. - id, err := tp.createRBFCompliantTx(req, m.feeFunc) + err := tp.createRBFCompliantTx(rid, req, m.feeFunc) // Check the result is as expected. require.ErrorIs(t, err, tc.expectedErr) - - // If there's an error, expect the requestID to be - // empty. - if tc.expectedErr != nil { - require.Zero(t, id) - } }) } } @@ -704,7 +697,8 @@ func TestTxPublisherBroadcast(t *testing.T) { // Create a testing record and put it in the map. fee := btcutil.Amount(1000) - requestID := tp.storeRecord(tx, req, m.feeFunc, fee, utxoIndex) + requestID := uint64(1) + tp.storeRecord(requestID, tx, req, m.feeFunc, fee, utxoIndex) // Quickly check when the requestID cannot be found, an error is // returned. @@ -799,6 +793,9 @@ func TestRemoveResult(t *testing.T) { op: 0, } + // Create a test request ID counter. + requestCounter := atomic.Uint64{} + testCases := []struct { name string setupRecord func() uint64 @@ -810,12 +807,13 @@ func TestRemoveResult(t *testing.T) { // removed. name: "remove on TxConfirmed", setupRecord: func() uint64 { - id := tp.storeRecord( - tx, req, m.feeFunc, fee, utxoIndex, + rid := requestCounter.Add(1) + tp.storeRecord( + rid, tx, req, m.feeFunc, fee, utxoIndex, ) - tp.subscriberChans.Store(id, nil) + tp.subscriberChans.Store(rid, nil) - return id + return rid }, result: &BumpResult{ Event: TxConfirmed, @@ -827,12 +825,13 @@ func TestRemoveResult(t *testing.T) { // When the tx is failed, the records will be removed. name: "remove on TxFailed", setupRecord: func() uint64 { - id := tp.storeRecord( - tx, req, m.feeFunc, fee, utxoIndex, + rid := requestCounter.Add(1) + tp.storeRecord( + rid, tx, req, m.feeFunc, fee, utxoIndex, ) - tp.subscriberChans.Store(id, nil) + tp.subscriberChans.Store(rid, nil) - return id + return rid }, result: &BumpResult{ Event: TxFailed, @@ -845,12 +844,13 @@ func TestRemoveResult(t *testing.T) { // Noop when the tx is neither confirmed or failed. name: "noop when tx is not confirmed or failed", setupRecord: func() uint64 { - id := tp.storeRecord( - tx, req, m.feeFunc, fee, utxoIndex, + rid := requestCounter.Add(1) + tp.storeRecord( + rid, tx, req, m.feeFunc, fee, utxoIndex, ) - tp.subscriberChans.Store(id, nil) + tp.subscriberChans.Store(rid, nil) - return id + return rid }, result: &BumpResult{ Event: TxPublished, @@ -905,7 +905,8 @@ func TestNotifyResult(t *testing.T) { // Create a testing record and put it in the map. fee := btcutil.Amount(1000) - requestID := tp.storeRecord(tx, req, m.feeFunc, fee, utxoIndex) + requestID := uint64(1) + tp.storeRecord(requestID, tx, req, m.feeFunc, fee, utxoIndex) // Create a subscription to the event. subscriber := make(chan *BumpResult, 1) @@ -953,41 +954,17 @@ func TestNotifyResult(t *testing.T) { } } -// TestBroadcastSuccess checks the public `Broadcast` method can successfully -// broadcast a tx based on the request. -func TestBroadcastSuccess(t *testing.T) { +// TestBroadcast checks the public `Broadcast` method can successfully register +// a broadcast request. +func TestBroadcast(t *testing.T) { t.Parallel() // Create a publisher using the mocks. - tp, m := createTestPublisher(t) + tp, _ := createTestPublisher(t) // Create a test feerate. feerate := chainfee.SatPerKWeight(1000) - // Mock the fee estimator to return the testing fee rate. - // - // We are not testing `NewLinearFeeFunction` here, so the actual params - // used are irrelevant. - m.estimator.On("EstimateFeePerKW", mock.Anything).Return( - feerate, nil).Once() - m.estimator.On("RelayFeePerKW").Return(chainfee.FeePerKwFloor).Once() - - // Mock the signer to always return a valid script. - // - // NOTE: we are not testing the utility of creating valid txes here, so - // this is fine to be mocked. This behaves essentially as skipping the - // Signer check and alaways assume the tx has a valid sig. - script := &input.Script{} - m.signer.On("ComputeInputScript", mock.Anything, - mock.Anything).Return(script, nil) - - // Mock the testmempoolaccept to pass. - m.wallet.On("CheckMempoolAcceptance", mock.Anything).Return(nil).Once() - - // Mock the wallet to publish successfully. - m.wallet.On("PublishTransaction", - mock.Anything, mock.Anything).Return(nil).Once() - // Create a test request. inp := createTestInput(1000, input.WitnessKeyHash) @@ -1003,25 +980,23 @@ func TestBroadcastSuccess(t *testing.T) { // Send the req and expect no error. resultChan, err := tp.Broadcast(req) require.NoError(t, err) - - // Check the result is sent back. - select { - case <-time.After(time.Second): - t.Fatal("timeout waiting for subscriber to receive result") - - case result := <-resultChan: - // We expect the first result to be TxPublished. - require.Equal(t, TxPublished, result.Event) - } + require.NotNil(t, resultChan) // Validate the record was stored. require.Equal(t, 1, tp.records.Len()) require.Equal(t, 1, tp.subscriberChans.Len()) + + // Validate the record. + rid := tp.requestCounter.Load() + record, found := tp.records.Load(rid) + require.True(t, found) + require.Equal(t, req, record.req) } -// TestBroadcastFail checks the public `Broadcast` returns the error or a -// failed result when the broadcast fails. -func TestBroadcastFail(t *testing.T) { +// TestBroadcastImmediate checks the public `Broadcast` method can successfully +// register a broadcast request and publish the tx when `Immediate` flag is +// set. +func TestBroadcastImmediate(t *testing.T) { t.Parallel() // Create a publisher using the mocks. @@ -1040,64 +1015,28 @@ func TestBroadcastFail(t *testing.T) { Budget: btcutil.Amount(1000), MaxFeeRate: feerate * 10, DeadlineHeight: 10, + Immediate: true, } - // Mock the fee estimator to return the testing fee rate. + // Mock the fee estimator to return an error. // - // We are not testing `NewLinearFeeFunction` here, so the actual params - // used are irrelevant. + // NOTE: We are not testing `handleInitialBroadcast` here, but only + // interested in checking that this method is indeed called when + // `Immediate` is true. Thus we mock the method to return an error to + // quickly abort. As long as this mocked method is called, we know the + // `Immediate` flag works. m.estimator.On("EstimateFeePerKW", mock.Anything).Return( - feerate, nil).Twice() - m.estimator.On("RelayFeePerKW").Return(chainfee.FeePerKwFloor).Twice() - - // Mock the signer to always return a valid script. - // - // NOTE: we are not testing the utility of creating valid txes here, so - // this is fine to be mocked. This behaves essentially as skipping the - // Signer check and alaways assume the tx has a valid sig. - script := &input.Script{} - m.signer.On("ComputeInputScript", mock.Anything, - mock.Anything).Return(script, nil) - - // Mock the testmempoolaccept to return an error. - m.wallet.On("CheckMempoolAcceptance", - mock.Anything).Return(errDummy).Once() + chainfee.SatPerKWeight(0), errDummy).Once() - // Send the req and expect an error returned. + // Send the req and expect no error. resultChan, err := tp.Broadcast(req) - require.ErrorIs(t, err, errDummy) - require.Nil(t, resultChan) - - // Validate the record was NOT stored. - require.Equal(t, 0, tp.records.Len()) - require.Equal(t, 0, tp.subscriberChans.Len()) - - // Mock the testmempoolaccept again, this time it passes. - m.wallet.On("CheckMempoolAcceptance", mock.Anything).Return(nil).Once() - - // Mock the wallet to fail on publish. - m.wallet.On("PublishTransaction", - mock.Anything, mock.Anything).Return(errDummy).Once() - - // Send the req and expect no error returned. - resultChan, err = tp.Broadcast(req) require.NoError(t, err) + require.NotNil(t, resultChan) - // Check the result is sent back. - select { - case <-time.After(time.Second): - t.Fatal("timeout waiting for subscriber to receive result") - - case result := <-resultChan: - // We expect the result to be TxFailed and the error is set in - // the result. - require.Equal(t, TxFailed, result.Event) - require.ErrorIs(t, result.Err, errDummy) - } - - // Validate the record was removed. - require.Equal(t, 0, tp.records.Len()) - require.Equal(t, 0, tp.subscriberChans.Len()) + // Validate the record was removed due to an error returned in initial + // broadcast. + require.Empty(t, tp.records.Len()) + require.Empty(t, tp.subscriberChans.Len()) } // TestCreateAnPublishFail checks all the error cases are handled properly in @@ -1270,7 +1209,8 @@ func TestHandleTxConfirmed(t *testing.T) { // Create a testing record and put it in the map. fee := btcutil.Amount(1000) - requestID := tp.storeRecord(tx, req, m.feeFunc, fee, utxoIndex) + requestID := uint64(1) + tp.storeRecord(requestID, tx, req, m.feeFunc, fee, utxoIndex) record, ok := tp.records.Load(requestID) require.True(t, ok) @@ -1350,7 +1290,8 @@ func TestHandleFeeBumpTx(t *testing.T) { // Create a testing record and put it in the map. fee := btcutil.Amount(1000) - requestID := tp.storeRecord(tx, req, m.feeFunc, fee, utxoIndex) + requestID := uint64(1) + tp.storeRecord(requestID, tx, req, m.feeFunc, fee, utxoIndex) // Create a subscription to the event. subscriber := make(chan *BumpResult, 1) @@ -1551,3 +1492,186 @@ func TestProcessRecords(t *testing.T) { require.Equal(t, requestID2, result.requestID) } } + +// TestHandleInitialBroadcastSuccess checks `handleInitialBroadcast` method can +// successfully broadcast a tx based on the request. +func TestHandleInitialBroadcastSuccess(t *testing.T) { + t.Parallel() + + // Create a publisher using the mocks. + tp, m := createTestPublisher(t) + + // Create a test feerate. + feerate := chainfee.SatPerKWeight(1000) + + // Mock the fee estimator to return the testing fee rate. + // + // We are not testing `NewLinearFeeFunction` here, so the actual params + // used are irrelevant. + m.estimator.On("EstimateFeePerKW", mock.Anything).Return( + feerate, nil).Once() + m.estimator.On("RelayFeePerKW").Return(chainfee.FeePerKwFloor).Once() + + // Mock the signer to always return a valid script. + // + // NOTE: we are not testing the utility of creating valid txes here, so + // this is fine to be mocked. This behaves essentially as skipping the + // Signer check and alaways assume the tx has a valid sig. + script := &input.Script{} + m.signer.On("ComputeInputScript", mock.Anything, + mock.Anything).Return(script, nil) + + // Mock the testmempoolaccept to pass. + m.wallet.On("CheckMempoolAcceptance", mock.Anything).Return(nil).Once() + + // Mock the wallet to publish successfully. + m.wallet.On("PublishTransaction", + mock.Anything, mock.Anything).Return(nil).Once() + + // Create a test request. + inp := createTestInput(1000, input.WitnessKeyHash) + + // Create a testing bump request. + req := &BumpRequest{ + DeliveryAddress: changePkScript, + Inputs: []input.Input{&inp}, + Budget: btcutil.Amount(1000), + MaxFeeRate: feerate * 10, + DeadlineHeight: 10, + } + + // Register the testing record use `Broadcast`. + resultChan, err := tp.Broadcast(req) + require.NoError(t, err) + + // Grab the monitor record from the map. + rid := tp.requestCounter.Load() + rec, ok := tp.records.Load(rid) + require.True(t, ok) + + // Call the method under test. + tp.wg.Add(1) + tp.handleInitialBroadcast(rec, rid) + + // Check the result is sent back. + select { + case <-time.After(time.Second): + t.Fatal("timeout waiting for subscriber to receive result") + + case result := <-resultChan: + // We expect the first result to be TxPublished. + require.Equal(t, TxPublished, result.Event) + } + + // Validate the record was stored. + require.Equal(t, 1, tp.records.Len()) + require.Equal(t, 1, tp.subscriberChans.Len()) +} + +// TestHandleInitialBroadcastFail checks `handleInitialBroadcast` returns the +// error or a failed result when the broadcast fails. +func TestHandleInitialBroadcastFail(t *testing.T) { + t.Parallel() + + // Create a publisher using the mocks. + tp, m := createTestPublisher(t) + + // Create a test feerate. + feerate := chainfee.SatPerKWeight(1000) + + // Create a test request. + inp := createTestInput(1000, input.WitnessKeyHash) + + // Create a testing bump request. + req := &BumpRequest{ + DeliveryAddress: changePkScript, + Inputs: []input.Input{&inp}, + Budget: btcutil.Amount(1000), + MaxFeeRate: feerate * 10, + DeadlineHeight: 10, + } + + // Mock the fee estimator to return the testing fee rate. + // + // We are not testing `NewLinearFeeFunction` here, so the actual params + // used are irrelevant. + m.estimator.On("EstimateFeePerKW", mock.Anything).Return( + feerate, nil).Twice() + m.estimator.On("RelayFeePerKW").Return(chainfee.FeePerKwFloor).Twice() + + // Mock the signer to always return a valid script. + // + // NOTE: we are not testing the utility of creating valid txes here, so + // this is fine to be mocked. This behaves essentially as skipping the + // Signer check and alaways assume the tx has a valid sig. + script := &input.Script{} + m.signer.On("ComputeInputScript", mock.Anything, + mock.Anything).Return(script, nil) + + // Mock the testmempoolaccept to return an error. + m.wallet.On("CheckMempoolAcceptance", + mock.Anything).Return(errDummy).Once() + + // Register the testing record use `Broadcast`. + resultChan, err := tp.Broadcast(req) + require.NoError(t, err) + + // Grab the monitor record from the map. + rid := tp.requestCounter.Load() + rec, ok := tp.records.Load(rid) + require.True(t, ok) + + // Call the method under test and expect an error returned. + tp.wg.Add(1) + tp.handleInitialBroadcast(rec, rid) + + // Check the result is sent back. + select { + case <-time.After(time.Second): + t.Fatal("timeout waiting for subscriber to receive result") + + case result := <-resultChan: + // We expect the first result to be TxFatal. + require.Equal(t, TxFatal, result.Event) + } + + // Validate the record was NOT stored. + require.Equal(t, 0, tp.records.Len()) + require.Equal(t, 0, tp.subscriberChans.Len()) + + // Mock the testmempoolaccept again, this time it passes. + m.wallet.On("CheckMempoolAcceptance", mock.Anything).Return(nil).Once() + + // Mock the wallet to fail on publish. + m.wallet.On("PublishTransaction", + mock.Anything, mock.Anything).Return(errDummy).Once() + + // Register the testing record use `Broadcast`. + resultChan, err = tp.Broadcast(req) + require.NoError(t, err) + + // Grab the monitor record from the map. + rid = tp.requestCounter.Load() + rec, ok = tp.records.Load(rid) + require.True(t, ok) + + // Call the method under test. + tp.wg.Add(1) + tp.handleInitialBroadcast(rec, rid) + + // Check the result is sent back. + select { + case <-time.After(time.Second): + t.Fatal("timeout waiting for subscriber to receive result") + + case result := <-resultChan: + // We expect the result to be TxFailed and the error is set in + // the result. + require.Equal(t, TxFailed, result.Event) + require.ErrorIs(t, result.Err, errDummy) + } + + // Validate the record was removed. + require.Equal(t, 0, tp.records.Len()) + require.Equal(t, 0, tp.subscriberChans.Len()) +} From 719ca5b229336ead40daf7d971e91feb726c3c10 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 30 Apr 2024 17:39:45 +0800 Subject: [PATCH 06/92] sweep: remove redundant error from `Broadcast` --- sweep/fee_bumper.go | 10 +++++----- sweep/fee_bumper_test.go | 15 +++++---------- sweep/mock_test.go | 6 +++--- sweep/sweeper.go | 11 +---------- sweep/sweeper_test.go | 9 ++------- 5 files changed, 16 insertions(+), 35 deletions(-) diff --git a/sweep/fee_bumper.go b/sweep/fee_bumper.go index 1e159fa1277..c2902bb60b4 100644 --- a/sweep/fee_bumper.go +++ b/sweep/fee_bumper.go @@ -65,7 +65,7 @@ type Bumper interface { // and monitors its confirmation status for potential fee bumping. It // returns a chan that the caller can use to receive updates about the // broadcast result and potential RBF attempts. - Broadcast(req *BumpRequest) (<-chan *BumpResult, error) + Broadcast(req *BumpRequest) <-chan *BumpResult } // BumpEvent represents the event of a fee bumping attempt. @@ -382,9 +382,9 @@ func (t *TxPublisher) isNeutrinoBackend() bool { // RBF-compliant unless the budget specified cannot cover the fee. // // NOTE: part of the Bumper interface. -func (t *TxPublisher) Broadcast(req *BumpRequest) (<-chan *BumpResult, error) { - log.Tracef("Received broadcast request: %s", lnutils.SpewLogClosure( - req)) +func (t *TxPublisher) Broadcast(req *BumpRequest) <-chan *BumpResult { + log.Tracef("Received broadcast request: %s", + lnutils.SpewLogClosure(req)) // Store the request. requestID, record := t.storeInitialRecord(req) @@ -398,7 +398,7 @@ func (t *TxPublisher) Broadcast(req *BumpRequest) (<-chan *BumpResult, error) { t.handleInitialBroadcast(record, requestID) } - return subscriber, nil + return subscriber } // storeInitialRecord initializes a monitor record and saves it in the map. diff --git a/sweep/fee_bumper_test.go b/sweep/fee_bumper_test.go index 50c1fdced60..ba41d65695e 100644 --- a/sweep/fee_bumper_test.go +++ b/sweep/fee_bumper_test.go @@ -978,8 +978,7 @@ func TestBroadcast(t *testing.T) { } // Send the req and expect no error. - resultChan, err := tp.Broadcast(req) - require.NoError(t, err) + resultChan := tp.Broadcast(req) require.NotNil(t, resultChan) // Validate the record was stored. @@ -1029,8 +1028,7 @@ func TestBroadcastImmediate(t *testing.T) { chainfee.SatPerKWeight(0), errDummy).Once() // Send the req and expect no error. - resultChan, err := tp.Broadcast(req) - require.NoError(t, err) + resultChan := tp.Broadcast(req) require.NotNil(t, resultChan) // Validate the record was removed due to an error returned in initial @@ -1541,8 +1539,7 @@ func TestHandleInitialBroadcastSuccess(t *testing.T) { } // Register the testing record use `Broadcast`. - resultChan, err := tp.Broadcast(req) - require.NoError(t, err) + resultChan := tp.Broadcast(req) // Grab the monitor record from the map. rid := tp.requestCounter.Load() @@ -1613,8 +1610,7 @@ func TestHandleInitialBroadcastFail(t *testing.T) { mock.Anything).Return(errDummy).Once() // Register the testing record use `Broadcast`. - resultChan, err := tp.Broadcast(req) - require.NoError(t, err) + resultChan := tp.Broadcast(req) // Grab the monitor record from the map. rid := tp.requestCounter.Load() @@ -1647,8 +1643,7 @@ func TestHandleInitialBroadcastFail(t *testing.T) { mock.Anything, mock.Anything).Return(errDummy).Once() // Register the testing record use `Broadcast`. - resultChan, err = tp.Broadcast(req) - require.NoError(t, err) + resultChan = tp.Broadcast(req) // Grab the monitor record from the map. rid = tp.requestCounter.Load() diff --git a/sweep/mock_test.go b/sweep/mock_test.go index d42b0320d45..f9471f22a0e 100644 --- a/sweep/mock_test.go +++ b/sweep/mock_test.go @@ -284,14 +284,14 @@ type MockBumper struct { var _ Bumper = (*MockBumper)(nil) // Broadcast broadcasts the transaction to the network. -func (m *MockBumper) Broadcast(req *BumpRequest) (<-chan *BumpResult, error) { +func (m *MockBumper) Broadcast(req *BumpRequest) <-chan *BumpResult { args := m.Called(req) if args.Get(0) == nil { - return nil, args.Error(1) + return nil } - return args.Get(0).(chan *BumpResult), args.Error(1) + return args.Get(0).(chan *BumpResult) } // MockFeeFunction is a mock implementation of the FeeFunction interface. diff --git a/sweep/sweeper.go b/sweep/sweeper.go index ed395480149..fb4f212fcac 100644 --- a/sweep/sweeper.go +++ b/sweep/sweeper.go @@ -838,16 +838,7 @@ func (s *UtxoSweeper) sweep(set InputSet) error { // Broadcast will return a read-only chan that we will listen to for // this publish result and future RBF attempt. - resp, err := s.cfg.Publisher.Broadcast(req) - if err != nil { - log.Errorf("Initial broadcast failed: %v, inputs=\n%v", err, - inputTypeSummary(set.Inputs())) - - // TODO(yy): find out which input is causing the failure. - s.markInputsPublishFailed(set) - - return err - } + resp := s.cfg.Publisher.Broadcast(req) // Successfully sent the broadcast attempt, we now handle the result by // subscribing to the result chan and listen for future updates about diff --git a/sweep/sweeper_test.go b/sweep/sweeper_test.go index 2e1be71c264..5cdbba29753 100644 --- a/sweep/sweeper_test.go +++ b/sweep/sweeper_test.go @@ -673,13 +673,8 @@ func TestSweepPendingInputs(t *testing.T) { setNeedWallet, normalSet, }) - // Mock `Broadcast` to return an error. This should cause the - // `createSweepTx` inside `sweep` to fail. This is done so we can - // terminate the method early as we are only interested in testing the - // workflow in `sweepPendingInputs`. We don't need to test `sweep` here - // as it should be tested in its own unit test. - dummyErr := errors.New("dummy error") - publisher.On("Broadcast", mock.Anything).Return(nil, dummyErr).Twice() + // Mock `Broadcast` to return a result. + publisher.On("Broadcast", mock.Anything).Return(nil).Twice() // Call the method under test. s.sweepPendingInputs(pis) From ba238962d607547c0c084fccde4ba72e05fd7dc3 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 30 Apr 2024 19:25:06 +0800 Subject: [PATCH 07/92] sweep: add method `handleBumpEventError` and fix `markInputFailed` Previously in `markInputFailed`, we'd remove all inputs under the same group via `removeExclusiveGroup`. This is wrong as when the current sweep fails for this input, it shouldn't affect other inputs. --- sweep/sweeper.go | 65 +++++++++++++++++++--- sweep/sweeper_test.go | 122 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 181 insertions(+), 6 deletions(-) diff --git a/sweep/sweeper.go b/sweep/sweeper.go index fb4f212fcac..0d1d40492d7 100644 --- a/sweep/sweeper.go +++ b/sweep/sweeper.go @@ -1441,11 +1441,6 @@ func (s *UtxoSweeper) markInputFailed(pi *SweeperInput, err error) { pi.state = Failed - // Remove all other inputs in this exclusive group. - if pi.params.ExclusiveGroup != nil { - s.removeExclusiveGroup(*pi.params.ExclusiveGroup) - } - s.signalResult(pi, Result{Err: err}) } @@ -1728,6 +1723,62 @@ func (s *UtxoSweeper) handleBumpEventTxPublished(resp *bumpResp) error { return nil } +// handleBumpEventTxFatal handles the case where there's an unexpected error +// when creating or publishing the sweeping tx. In this case, the tx will be +// removed from the sweeper store and the inputs will be marked as `Failed`, +// which means they will not be retried. +func (s *UtxoSweeper) handleBumpEventTxFatal(resp *bumpResp) error { + r := resp.result + + // Remove the tx from the sweeper store if there is one. Since this is + // a broadcast error, it's likely there isn't a tx here. + if r.Tx != nil { + txid := r.Tx.TxHash() + log.Infof("Tx=%v failed with unexpected error: %v", txid, r.Err) + + // Remove the tx from the sweeper db if it exists. + if err := s.cfg.Store.DeleteTx(txid); err != nil { + return fmt.Errorf("delete tx record for %v: %w", txid, + err) + } + } + + // Mark the inputs as failed. + s.markInputsFailed(resp.set, r.Err) + + return nil +} + +// markInputsFailed marks all inputs found in the tx as failed. It will also +// notify all the subscribers of these inputs. +func (s *UtxoSweeper) markInputsFailed(set InputSet, err error) { + for _, inp := range set.Inputs() { + outpoint := inp.OutPoint() + + input, ok := s.inputs[outpoint] + if !ok { + // It's very likely that a spending tx contains inputs + // that we don't know. + log.Tracef("Skipped marking input as failed: %v not "+ + "found in pending inputs", outpoint) + + continue + } + + // If the input is already in a terminal state, we don't want + // to rewrite it, which also indicates an error as we only get + // an error event during the initial broadcast. + if input.terminated() { + log.Errorf("Skipped marking input=%v as failed due to "+ + "unexpected state=%v", outpoint, input.state) + + continue + } + + s.markInputFailed(input, err) + } +} + // handleBumpEvent handles the result sent from the bumper based on its event // type. // @@ -1752,8 +1803,10 @@ func (s *UtxoSweeper) handleBumpEvent(r *bumpResp) error { case TxReplaced: return s.handleBumpEventTxReplaced(r) + // There's a fatal error in creating the tx, we will remove the tx from + // the sweeper db and mark the inputs as failed. case TxFatal: - // TODO(yy): create a method to remove this input. + return s.handleBumpEventTxFatal(r) } return nil diff --git a/sweep/sweeper_test.go b/sweep/sweeper_test.go index 5cdbba29753..39b758cb9bf 100644 --- a/sweep/sweeper_test.go +++ b/sweep/sweeper_test.go @@ -1075,3 +1075,125 @@ func TestMonitorFeeBumpResult(t *testing.T) { }) } } + +// TestMarkInputsFailed checks that given a list of inputs with different +// states, the method `markInputsFailed` correctly marks the inputs as failed. +func TestMarkInputsFailed(t *testing.T) { + t.Parallel() + + require := require.New(t) + + // Create a mock input set. + set := &MockInputSet{} + defer set.AssertExpectations(t) + + // Create a test sweeper. + s := New(&UtxoSweeperConfig{}) + + // Create testing inputs for each state. + // - inputInit specifies a newly created input. When marking this as + // published, we should see an error log as this input hasn't been + // published yet. + // - inputPendingPublish specifies an input about to be published. + // - inputPublished specifies an input that's published. + // - inputPublishFailed specifies an input that's failed to be + // published. + // - inputSwept specifies an input that's swept. + // - inputExcluded specifies an input that's excluded. + // - inputFailed specifies an input that's failed. + var ( + inputInit = createMockInput(t, s, Init) + inputPendingPublish = createMockInput(t, s, PendingPublish) + inputPublished = createMockInput(t, s, Published) + inputPublishFailed = createMockInput(t, s, PublishFailed) + inputSwept = createMockInput(t, s, Swept) + inputExcluded = createMockInput(t, s, Excluded) + inputFailed = createMockInput(t, s, Failed) + ) + + // Gather all inputs. + set.On("Inputs").Return([]input.Input{ + inputInit, inputPendingPublish, inputPublished, + inputPublishFailed, inputSwept, inputExcluded, inputFailed, + }) + + // Mark the test inputs. We expect the non-exist input and + // inputSwept/inputExcluded/inputFailed to be skipped. + s.markInputsFailed(set, errDummy) + + // We expect unchanged number of pending inputs. + require.Len(s.inputs, 7) + + // We expect the init input's to be marked as failed. + require.Equal(Failed, s.inputs[inputInit.OutPoint()].state) + + // We expect the pending-publish input to be marked as failed. + require.Equal(Failed, s.inputs[inputPendingPublish.OutPoint()].state) + + // We expect the published input to be marked as failed. + require.Equal(Failed, s.inputs[inputPublished.OutPoint()].state) + + // We expect the publish failed input to be markd as failed. + require.Equal(Failed, s.inputs[inputPublishFailed.OutPoint()].state) + + // We expect the swept input to stay unchanged. + require.Equal(Swept, s.inputs[inputSwept.OutPoint()].state) + + // We expect the excluded input to stay unchanged. + require.Equal(Excluded, s.inputs[inputExcluded.OutPoint()].state) + + // We expect the failed input to stay unchanged. + require.Equal(Failed, s.inputs[inputFailed.OutPoint()].state) +} + +// TestHandleBumpEventTxFatal checks that `handleBumpEventTxFatal` correctly +// handles a `TxFatal` event. +func TestHandleBumpEventTxFatal(t *testing.T) { + t.Parallel() + + rt := require.New(t) + + // Create a mock store. + store := &MockSweeperStore{} + defer store.AssertExpectations(t) + + // Create a mock input set. We are not testing `markInputFailed` here, + // so the actual set doesn't matter. + set := &MockInputSet{} + defer set.AssertExpectations(t) + set.On("Inputs").Return(nil) + + // Create a test sweeper. + s := New(&UtxoSweeperConfig{ + Store: store, + }) + + // Create a dummy tx. + tx := &wire.MsgTx{ + LockTime: 1, + } + + // Create a testing bump response. + result := &BumpResult{ + Err: errDummy, + Tx: tx, + } + resp := &bumpResp{ + result: result, + set: set, + } + + // Mock the store to return an error. + store.On("DeleteTx", mock.Anything).Return(errDummy).Once() + + // Call the method under test and assert the error is returned. + err := s.handleBumpEventTxFatal(resp) + rt.ErrorIs(err, errDummy) + + // Mock the store to return nil. + store.On("DeleteTx", mock.Anything).Return(nil).Once() + + // Call the method under test and assert no error is returned. + err = s.handleBumpEventTxFatal(resp) + rt.NoError(err) +} From afc08c66231d3980bacf1702a2357844adec12f6 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 30 Apr 2024 19:28:01 +0800 Subject: [PATCH 08/92] sweep: add method `isMature` on `SweeperInput` Also updated `handlePendingSweepsReq` to skip immature inputs so the returned results are the same as those in pre-0.18.0. --- sweep/sweeper.go | 51 +++++++++++++++++++++++++++++++------------ sweep/sweeper_test.go | 2 ++ 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/sweep/sweeper.go b/sweep/sweeper.go index 0d1d40492d7..7f794431f3b 100644 --- a/sweep/sweeper.go +++ b/sweep/sweeper.go @@ -222,6 +222,34 @@ func (p *SweeperInput) terminated() bool { } } +// isMature returns a boolean indicating whether the input has a timelock that +// has been reached or not. The locktime found is also returned. +func (p *SweeperInput) isMature(currentHeight uint32) (bool, uint32) { + locktime, _ := p.RequiredLockTime() + if currentHeight < locktime { + log.Debugf("Input %v has locktime=%v, current height is %v", + p.OutPoint(), locktime, currentHeight) + + return false, locktime + } + + // If the input has a CSV that's not yet reached, we will skip + // this input and wait for the expiry. + // + // NOTE: We need to consider whether this input can be included in the + // next block or not, which means the CSV will be checked against the + // currentHeight plus one. + locktime = p.BlocksToMaturity() + p.HeightHint() + if currentHeight+1 < locktime { + log.Debugf("Input %v has CSV expiry=%v, current height is %v", + p.OutPoint(), locktime, currentHeight) + + return false, locktime + } + + return true, locktime +} + // InputsMap is a type alias for a set of pending inputs. type InputsMap = map[wire.OutPoint]*SweeperInput @@ -1038,6 +1066,12 @@ func (s *UtxoSweeper) handlePendingSweepsReq( resps := make(map[wire.OutPoint]*PendingInputResponse, len(s.inputs)) for _, inp := range s.inputs { + // Skip immature inputs for compatibility. + mature, _ := inp.isMature(uint32(s.currentHeight)) + if !mature { + continue + } + // Only the exported fields are set, as we expect the response // to only be consumed externally. op := inp.OutPoint() @@ -1485,20 +1519,9 @@ func (s *UtxoSweeper) updateSweeperInputs() InputsMap { // If the input has a locktime that's not yet reached, we will // skip this input and wait for the locktime to be reached. - locktime, _ := input.RequiredLockTime() - if uint32(s.currentHeight) < locktime { - log.Warnf("Skipping input %v due to locktime=%v not "+ - "reached, current height is %v", op, locktime, - s.currentHeight) - - continue - } - - // If the input has a CSV that's not yet reached, we will skip - // this input and wait for the expiry. - locktime = input.BlocksToMaturity() + input.HeightHint() - if s.currentHeight < int32(locktime)-1 { - log.Infof("Skipping input %v due to CSV expiry=%v not "+ + mature, locktime := input.isMature(uint32(s.currentHeight)) + if !mature { + log.Infof("Skipping input %v due to locktime=%v not "+ "reached, current height is %v", op, locktime, s.currentHeight) diff --git a/sweep/sweeper_test.go b/sweep/sweeper_test.go index 39b758cb9bf..5b827aff890 100644 --- a/sweep/sweeper_test.go +++ b/sweep/sweeper_test.go @@ -443,6 +443,7 @@ func TestUpdateSweeperInputs(t *testing.T) { // returned. inp2.On("RequiredLockTime").Return( uint32(s.currentHeight+1), true).Once() + inp2.On("OutPoint").Return(wire.OutPoint{Index: 2}).Maybe() input7 := &SweeperInput{state: Init, Input: inp2} // Mock the input to have a CSV expiry in the future so it will NOT be @@ -451,6 +452,7 @@ func TestUpdateSweeperInputs(t *testing.T) { uint32(s.currentHeight), false).Once() inp3.On("BlocksToMaturity").Return(uint32(2)).Once() inp3.On("HeightHint").Return(uint32(s.currentHeight)).Once() + inp3.On("OutPoint").Return(wire.OutPoint{Index: 3}).Maybe() input8 := &SweeperInput{state: Init, Input: inp3} // Add the inputs to the sweeper. After the update, we should see the From 7545bbfa9274b1cea2854b09b3714325fdfd60d8 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Wed, 1 May 2024 02:58:27 +0800 Subject: [PATCH 09/92] sweep: make sure defaultDeadline is derived from the mature height --- sweep/sweeper.go | 54 +++++++++++++++++++++++++++++++++++-------- sweep/sweeper_test.go | 2 +- 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/sweep/sweeper.go b/sweep/sweeper.go index 7f794431f3b..813d0b0ca0d 100644 --- a/sweep/sweeper.go +++ b/sweep/sweeper.go @@ -530,7 +530,7 @@ func (s *UtxoSweeper) SweepInput(inp input.Input, } absoluteTimeLock, _ := inp.RequiredLockTime() - log.Infof("Sweep request received: out_point=%v, witness_type=%v, "+ + log.Debugf("Sweep request received: out_point=%v, witness_type=%v, "+ "relative_time_lock=%v, absolute_time_lock=%v, amount=%v, "+ "parent=(%v), params=(%v)", inp.OutPoint(), inp.WitnessType(), inp.BlocksToMaturity(), absoluteTimeLock, @@ -736,7 +736,18 @@ func (s *UtxoSweeper) collector(blockEpochs <-chan *chainntnfs.BlockEpoch) { inputs := s.updateSweeperInputs() log.Debugf("Received new block: height=%v, attempt "+ - "sweeping %d inputs", epoch.Height, len(inputs)) + "sweeping %d inputs:\n%s", + epoch.Height, len(inputs), + lnutils.NewLogClosure(func() string { + inps := make( + []input.Input, 0, len(inputs), + ) + for _, in := range inputs { + inps = append(inps, in) + } + + return inputTypeSummary(inps) + })) // Attempt to sweep any pending inputs. s.sweepPendingInputs(inputs) @@ -1207,13 +1218,29 @@ func (s *UtxoSweeper) mempoolLookup(op wire.OutPoint) fn.Option[wire.MsgTx] { return s.cfg.Mempool.LookupInputMempoolSpend(op) } -// handleNewInput processes a new input by registering spend notification and -// scheduling sweeping for it. -func (s *UtxoSweeper) handleNewInput(input *sweepInputMessage) error { +// calculateDefaultDeadline calculates the default deadline height for a sweep +// request that has no deadline height specified. +func (s *UtxoSweeper) calculateDefaultDeadline(pi *SweeperInput) int32 { // Create a default deadline height, which will be used when there's no // DeadlineHeight specified for a given input. defaultDeadline := s.currentHeight + int32(s.cfg.NoDeadlineConfTarget) + // If the input is immature and has a locktime, we'll use the locktime + // height as the starting height. + matured, locktime := pi.isMature(uint32(s.currentHeight)) + if !matured { + defaultDeadline = int32(locktime + s.cfg.NoDeadlineConfTarget) + log.Debugf("Input %v is immature, using locktime=%v instead "+ + "of current height=%d", pi.OutPoint(), locktime, + s.currentHeight) + } + + return defaultDeadline +} + +// handleNewInput processes a new input by registering spend notification and +// scheduling sweeping for it. +func (s *UtxoSweeper) handleNewInput(input *sweepInputMessage) error { outpoint := input.input.OutPoint() pi, pending := s.inputs[outpoint] if pending { @@ -1238,15 +1265,22 @@ func (s *UtxoSweeper) handleNewInput(input *sweepInputMessage) error { Input: input.input, params: input.params, rbf: rbfInfo, - // Set the acutal deadline height. - DeadlineHeight: input.params.DeadlineHeight.UnwrapOr( - defaultDeadline, - ), } + // Set the acutal deadline height. + pi.DeadlineHeight = input.params.DeadlineHeight.UnwrapOr( + s.calculateDefaultDeadline(pi), + ) + s.inputs[outpoint] = pi log.Tracef("input %v, state=%v, added to inputs", outpoint, pi.state) + log.Infof("Registered sweep request at block %d: out_point=%v, "+ + "witness_type=%v, amount=%v, deadline=%d, params=(%v)", + s.currentHeight, pi.OutPoint(), pi.WitnessType(), + btcutil.Amount(pi.SignDesc().Output.Value), pi.DeadlineHeight, + pi.params) + // Start watching for spend of this input, either by us or the remote // party. cancel, err := s.monitorSpend( @@ -1660,7 +1694,7 @@ func (s *UtxoSweeper) handleBumpEventTxFailed(resp *bumpResp) { r := resp.result tx, err := r.Tx, r.Err - log.Errorf("Fee bump attempt failed for tx=%v: %v", tx.TxHash(), err) + log.Warnf("Fee bump attempt failed for tx=%v: %v", tx.TxHash(), err) // NOTE: When marking the inputs as failed, we are using the input set // instead of the inputs found in the tx. This is fine for current diff --git a/sweep/sweeper_test.go b/sweep/sweeper_test.go index 5b827aff890..16a4a46fbe5 100644 --- a/sweep/sweeper_test.go +++ b/sweep/sweeper_test.go @@ -739,7 +739,7 @@ func TestHandleBumpEventTxFailed(t *testing.T) { // Call the method under test. err := s.handleBumpEvent(resp) - require.ErrorIs(t, err, errDummy) + require.NoError(t, err) // Assert the states of the first two inputs are updated. require.Equal(t, PublishFailed, s.inputs[op1].state) From f0c4e6dba12ab6f35c45bcf88cd39018734df0f0 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 25 Oct 2024 15:17:41 +0800 Subject: [PATCH 10/92] sweep: remove redundant loopvar assign --- sweep/fee_bumper.go | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/sweep/fee_bumper.go b/sweep/fee_bumper.go index c2902bb60b4..c08a0a762b9 100644 --- a/sweep/fee_bumper.go +++ b/sweep/fee_bumper.go @@ -916,11 +916,9 @@ func (t *TxPublisher) processRecords() { // For records that are confirmed, we'll notify the caller about this // result. for requestID, r := range confirmedRecords { - rec := r - log.Debugf("Tx=%v is confirmed", r.tx.TxHash()) t.wg.Add(1) - go t.handleTxConfirmed(rec, requestID) + go t.handleTxConfirmed(r, requestID) } // Get the current height to be used in the following goroutines. @@ -928,22 +926,18 @@ func (t *TxPublisher) processRecords() { // For records that are not confirmed, we perform a fee bump if needed. for requestID, r := range feeBumpRecords { - rec := r - log.Debugf("Attempting to fee bump Tx=%v", r.tx.TxHash()) t.wg.Add(1) - go t.handleFeeBumpTx(requestID, rec, currentHeight) + go t.handleFeeBumpTx(requestID, r, currentHeight) } // For records that are failed, we'll notify the caller about this // result. for requestID, r := range failedRecords { - rec := r - log.Debugf("Tx=%v has inputs been spent by a third party, "+ "failing it now", r.tx.TxHash()) t.wg.Add(1) - go t.handleThirdPartySpent(rec, requestID) + go t.handleThirdPartySpent(r, requestID) } } From 78ce757e7bdbb27c8c1bfccc6df5b924bcc56e4c Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 7 Nov 2024 15:07:26 +0800 Subject: [PATCH 11/92] sweep: break `initialBroadcast` into two steps With the combination of the following commit we can have a more granular control over the bump result when handling it in the sweeper. --- sweep/fee_bumper.go | 111 ++++++++++++++++++++++++++------------------ 1 file changed, 65 insertions(+), 46 deletions(-) diff --git a/sweep/fee_bumper.go b/sweep/fee_bumper.go index c08a0a762b9..181f2748394 100644 --- a/sweep/fee_bumper.go +++ b/sweep/fee_bumper.go @@ -417,31 +417,23 @@ func (t *TxPublisher) storeInitialRecord(req *BumpRequest) ( return requestID, record } -// initialBroadcast initializes a fee function, creates an RBF-compliant tx and -// broadcasts it. -func (t *TxPublisher) initialBroadcast(requestID uint64, - req *BumpRequest) (*BumpResult, error) { - +// initializeTx initializes a fee function and creates an RBF-compliant tx. If +// succeeded, the initial tx is stored in the records map. +func (t *TxPublisher) initializeTx(requestID uint64, req *BumpRequest) error { // Create a fee bumping algorithm to be used for future RBF. feeAlgo, err := t.initializeFeeFunction(req) if err != nil { - return nil, fmt.Errorf("init fee function: %w", err) + return fmt.Errorf("init fee function: %w", err) } // Create the initial tx to be broadcasted. This tx is guaranteed to // comply with the RBF restrictions. err = t.createRBFCompliantTx(requestID, req, feeAlgo) if err != nil { - return nil, fmt.Errorf("create RBF-compliant tx: %w", err) - } - - // Broadcast the tx and return the monitored record. - result, err := t.broadcast(requestID) - if err != nil { - return nil, fmt.Errorf("broadcast sweep tx: %w", err) + return fmt.Errorf("create RBF-compliant tx: %w", err) } - return result, nil + return nil } // initializeFeeFunction initializes a fee function to be used for this request @@ -962,6 +954,50 @@ func (t *TxPublisher) handleTxConfirmed(r *monitorRecord, requestID uint64) { t.handleResult(result) } +// handleInitialTxError takes the error from `initializeTx` and decides the +// bump event. It will construct a BumpResult and handles it. +func (t *TxPublisher) handleInitialTxError(requestID uint64, err error) { + // We now decide what type of event to send. + var event BumpEvent + + switch { + // When the error is due to a dust output, we'll send a TxFailed so + // these inputs can be retried with a different group in the next + // block. + case errors.Is(err, ErrTxNoOutput): + event = TxFailed + + // When the error is due to budget being used up, we'll send a TxFailed + // so these inputs can be retried with a different group in the next + // block. + case errors.Is(err, ErrMaxPosition): + event = TxFailed + + // When the error is due to zero fee rate delta, we'll send a TxFailed + // so these inputs can be retried in the next block. + case errors.Is(err, ErrZeroFeeRateDelta): + event = TxFailed + + // Otherwise this is not a fee-related error and the tx cannot be + // retried. In that case we will fail ALL the inputs in this tx, which + // means they will be removed from the sweeper and never be tried + // again. + // + // TODO(yy): Find out which input is causing the failure and fail that + // one only. + default: + event = TxFatal + } + + result := &BumpResult{ + Event: event, + Err: err, + requestID: requestID, + } + + t.handleResult(result) +} + // handleInitialBroadcast is called when a new request is received. It will // handle the initial tx creation and broadcast. In details, // 1. init a fee function based on the given strategy. @@ -979,44 +1015,27 @@ func (t *TxPublisher) handleInitialBroadcast(r *monitorRecord, // Attempt an initial broadcast which is guaranteed to comply with the // RBF rules. - result, err = t.initialBroadcast(requestID, r.req) + // + // Create the initial tx to be broadcasted. + err = t.initializeTx(requestID, r.req) if err != nil { log.Errorf("Initial broadcast failed: %v", err) - // We now decide what type of event to send. - var event BumpEvent + // We now handle the initialization error and exit. + t.handleInitialTxError(requestID, err) - switch { - // When the error is due to a dust output, we'll send a - // TxFailed so these inputs can be retried with a different - // group in the next block. - case errors.Is(err, ErrTxNoOutput): - event = TxFailed - - // When the error is due to budget being used up, we'll send a - // TxFailed so these inputs can be retried with a different - // group in the next block. - case errors.Is(err, ErrMaxPosition): - event = TxFailed - - // When the error is due to zero fee rate delta, we'll send a - // TxFailed so these inputs can be retried in the next block. - case errors.Is(err, ErrZeroFeeRateDelta): - event = TxFailed - - // Otherwise this is not a fee-related error and the tx cannot - // be retried. In that case we will fail ALL the inputs in this - // tx, which means they will be removed from the sweeper and - // never be tried again. - // - // TODO(yy): Find out which input is causing the failure and - // fail that one only. - default: - event = TxFatal - } + return + } + // Successfully created the first tx, now broadcast it. + result, err = t.broadcast(requestID) + if err != nil { + // The broadcast failed, which can only happen if the tx record + // cannot be found or the aux sweeper returns an error. In + // either case, we will send back a TxFail event so these + // inputs can be retried. result = &BumpResult{ - Event: event, + Event: TxFailed, Err: err, requestID: requestID, } From 30ee45009111603d0e1171df6e36aaa3f265ed6c Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 7 Nov 2024 20:28:50 +0800 Subject: [PATCH 12/92] sweep: make sure nil tx is handled After previous commit, it should be clear that the tx may be failed to created in a `TxFailed` event. We now make sure to catch it to avoid panic. --- sweep/fee_bumper.go | 22 ++++++++++++++++------ sweep/fee_bumper_test.go | 14 +++++++------- sweep/sweeper.go | 13 ++++++++++++- 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/sweep/fee_bumper.go b/sweep/fee_bumper.go index 181f2748394..13fa1272c68 100644 --- a/sweep/fee_bumper.go +++ b/sweep/fee_bumper.go @@ -75,7 +75,17 @@ const ( // TxPublished is sent when the broadcast attempt is finished. TxPublished BumpEvent = iota - // TxFailed is sent when the broadcast attempt fails. + // TxFailed is sent when the tx has encountered a fee-related error + // during its creation or broadcast, or an internal error from the fee + // bumper. In either case the inputs in this tx should be retried with + // either a different grouping strategy or an increased budget. + // + // NOTE: We also send this event when there's a third party spend + // event, and the sweeper will handle cleaning this up once it's + // confirmed. + // + // TODO(yy): Remove the above usage once we remove sweeping non-CPFP + // anchors. TxFailed // TxReplaced is sent when the original tx is replaced by a new one. @@ -269,8 +279,10 @@ func (b *BumpResult) String() string { // Validate validates the BumpResult so it's safe to use. func (b *BumpResult) Validate() error { + isFailureEvent := b.Event == TxFailed || b.Event == TxFatal + // Every result must have a tx except the fatal or failed case. - if b.Tx == nil && b.Event != TxFatal { + if b.Tx == nil && !isFailureEvent { return fmt.Errorf("%w: nil tx", ErrInvalidBumpResult) } @@ -285,10 +297,8 @@ func (b *BumpResult) Validate() error { } // If it's a failed or fatal event, it must have an error. - if b.Event == TxFatal || b.Event == TxFailed { - if b.Err == nil { - return fmt.Errorf("%w: nil error", ErrInvalidBumpResult) - } + if isFailureEvent && b.Err == nil { + return fmt.Errorf("%w: nil error", ErrInvalidBumpResult) } // If it's a confirmed event, it must have a fee rate and fee. diff --git a/sweep/fee_bumper_test.go b/sweep/fee_bumper_test.go index ba41d65695e..54c67dbe281 100644 --- a/sweep/fee_bumper_test.go +++ b/sweep/fee_bumper_test.go @@ -91,13 +91,6 @@ func TestBumpResultValidate(t *testing.T) { } require.ErrorIs(t, b.Validate(), ErrInvalidBumpResult) - // A failed event without a tx will give an error. - b = BumpResult{ - Event: TxFailed, - Err: errDummy, - } - require.ErrorIs(t, b.Validate(), ErrInvalidBumpResult) - // A fatal event without a failure reason will give an error. b = BumpResult{ Event: TxFailed, @@ -118,6 +111,13 @@ func TestBumpResultValidate(t *testing.T) { } require.NoError(t, b.Validate()) + // Tx is allowed to be nil in a TxFailed event. + b = BumpResult{ + Event: TxFailed, + Err: errDummy, + } + require.NoError(t, b.Validate()) + // Tx is allowed to be nil in a TxFatal event. b = BumpResult{ Event: TxFatal, diff --git a/sweep/sweeper.go b/sweep/sweeper.go index 813d0b0ca0d..976fceff314 100644 --- a/sweep/sweeper.go +++ b/sweep/sweeper.go @@ -1669,6 +1669,14 @@ func (s *UtxoSweeper) monitorFeeBumpResult(set InputSet, // in sweeper and rely solely on this event to mark // inputs as Swept? if r.Event == TxConfirmed || r.Event == TxFailed { + // Exit if the tx is failed to be created. + if r.Tx == nil { + log.Debugf("Received %v for nil tx, "+ + "exit monitor", r.Event) + + return + } + log.Debugf("Received %v for sweep tx %v, exit "+ "fee bump monitor", r.Event, r.Tx.TxHash()) @@ -1694,7 +1702,10 @@ func (s *UtxoSweeper) handleBumpEventTxFailed(resp *bumpResp) { r := resp.result tx, err := r.Tx, r.Err - log.Warnf("Fee bump attempt failed for tx=%v: %v", tx.TxHash(), err) + if tx != nil { + log.Warnf("Fee bump attempt failed for tx=%v: %v", tx.TxHash(), + err) + } // NOTE: When marking the inputs as failed, we are using the input set // instead of the inputs found in the tx. This is fine for current From 060ff013c14bbd4e3ec8a3c570b485e7d52baba2 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 27 Jun 2024 08:36:19 +0800 Subject: [PATCH 13/92] chainio: introduce `chainio` to handle block synchronization This commit inits the package `chainio` and defines the interface `Blockbeat` and `Consumer`. The `Consumer` must be implemented by other subsystems if it requires block epoch subscription. --- chainio/README.md | 152 +++++++++++++++++++++++++++++++++++++++++++ chainio/interface.go | 40 ++++++++++++ chainio/log.go | 32 +++++++++ 3 files changed, 224 insertions(+) create mode 100644 chainio/README.md create mode 100644 chainio/interface.go create mode 100644 chainio/log.go diff --git a/chainio/README.md b/chainio/README.md new file mode 100644 index 00000000000..b11e38157c2 --- /dev/null +++ b/chainio/README.md @@ -0,0 +1,152 @@ +# Chainio + +`chainio` is a package designed to provide blockchain data access to various +subsystems within `lnd`. When a new block is received, it is encapsulated in a +`Blockbeat` object and disseminated to all registered consumers. Consumers may +receive these updates either concurrently or sequentially, based on their +registration configuration, ensuring that each subsystem maintains a +synchronized view of the current block state. + +The main components include: + +- `Blockbeat`: An interface that provides information about the block. + +- `Consumer`: An interface that specifies how subsystems handle the blockbeat. + +- `BlockbeatDispatcher`: The core service responsible for receiving each block + and distributing it to all consumers. + +Additionally, the `BeatConsumer` struct provides a partial implementation of +the `Consumer` interface. This struct helps reduce code duplication, allowing +subsystems to avoid re-implementing the `ProcessBlock` method and provides a +commonly used `NotifyBlockProcessed` method. + + +### Register a Consumer + +Consumers within the same queue are notified **sequentially**, while all queues +are notified **concurrently**. A queue consists of a slice of consumers, which +are notified in left-to-right order. Developers are responsible for determining +dependencies in block consumption across subsystems: independent subsystems +should be notified concurrently, whereas dependent subsystems should be +notified sequentially. + +To notify the consumers concurrently, put them in different queues, +```go +// consumer1 and consumer2 will be notified concurrently. +queue1 := []chainio.Consumer{consumer1} +blockbeatDispatcher.RegisterQueue(consumer1) + +queue2 := []chainio.Consumer{consumer2} +blockbeatDispatcher.RegisterQueue(consumer2) +``` + +To notify the consumers sequentially, put them in the same queue, +```go +// consumers will be notified sequentially via, +// consumer1 -> consumer2 -> consumer3 +queue := []chainio.Consumer{ + consumer1, + consumer2, + consumer3, +} +blockbeatDispatcher.RegisterQueue(queue) +``` + +### Implement the `Consumer` Interface + +Implementing the `Consumer` interface is straightforward. Below is an example +of how +[`sweep.TxPublisher`](https://github.com/lightningnetwork/lnd/blob/5cec466fad44c582a64cfaeb91f6d5fd302fcf85/sweep/fee_bumper.go#L310) +implements this interface. + +To start, embed the partial implementation `chainio.BeatConsumer`, which +already provides the `ProcessBlock` implementation and commonly used +`NotifyBlockProcessed` method, and exposes `BlockbeatChan` for the consumer to +receive blockbeats. + +```go +type TxPublisher struct { + started atomic.Bool + stopped atomic.Bool + + chainio.BeatConsumer + + ... +``` + +We should also remember to initialize this `BeatConsumer`, + +```go +... +// Mount the block consumer. +tp.BeatConsumer = chainio.NewBeatConsumer(tp.quit, tp.Name()) +``` + +Finally, in the main event loop, read from `BlockbeatChan`, process the +received blockbeat, and, crucially, call `tp.NotifyBlockProcessed` to inform +the blockbeat dispatcher that processing is complete. + +```go +for { + select { + case beat := <-tp.BlockbeatChan: + // Consume this blockbeat, usually it means updating the subsystem + // using the new block data. + + // Notify we've processed the block. + tp.NotifyBlockProcessed(beat, nil) + + ... +``` + +### Existing Queues + +Currently, we have a single queue of consumers dedicated to handling force +closures. This queue includes `ChainArbitrator`, `UtxoSweeper`, and +`TxPublisher`, with `ChainArbitrator` managing two internal consumers: +`chainWatcher` and `ChannelArbitrator`. The blockbeat flows sequentially +through the chain as follows: `ChainArbitrator => chainWatcher => +ChannelArbitrator => UtxoSweeper => TxPublisher`. The following diagram +illustrates the flow within the public subsystems. + +```mermaid +sequenceDiagram + autonumber + participant bb as BlockBeat + participant cc as ChainArb + participant us as UtxoSweeper + participant tp as TxPublisher + + note left of bb: 0. received block x,
dispatching... + + note over bb,cc: 1. send block x to ChainArb,
wait for its done signal + bb->>cc: block x + rect rgba(165, 0, 85, 0.8) + critical signal processed + cc->>bb: processed block + option Process error or timeout + bb->>bb: error and exit + end + end + + note over bb,us: 2. send block x to UtxoSweeper, wait for its done signal + bb->>us: block x + rect rgba(165, 0, 85, 0.8) + critical signal processed + us->>bb: processed block + option Process error or timeout + bb->>bb: error and exit + end + end + + note over bb,tp: 3. send block x to TxPublisher, wait for its done signal + bb->>tp: block x + rect rgba(165, 0, 85, 0.8) + critical signal processed + tp->>bb: processed block + option Process error or timeout + bb->>bb: error and exit + end + end +``` diff --git a/chainio/interface.go b/chainio/interface.go new file mode 100644 index 00000000000..70827f20763 --- /dev/null +++ b/chainio/interface.go @@ -0,0 +1,40 @@ +package chainio + +// Blockbeat defines an interface that can be used by subsystems to retrieve +// block data. It is sent by the BlockbeatDispatcher whenever a new block is +// received. Once the subsystem finishes processing the block, it must signal +// it by calling NotifyBlockProcessed. +// +// The blockchain is a state machine - whenever there's a state change, it's +// manifested in a block. The blockbeat is a way to notify subsystems of this +// state change, and to provide them with the data they need to process it. In +// other words, subsystems must react to this state change and should consider +// being driven by the blockbeat in their own state machines. +type Blockbeat interface { + // Height returns the current block height. + Height() int32 +} + +// Consumer defines a blockbeat consumer interface. Subsystems that need block +// info must implement it. +type Consumer interface { + // TODO(yy): We should also define the start methods used by the + // consumers such that when implementing the interface, the consumer + // will always be started with a blockbeat. This cannot be enforced at + // the moment as we need refactor all the start methods to only take a + // beat. + // + // Start(beat Blockbeat) error + + // Name returns a human-readable string for this subsystem. + Name() string + + // ProcessBlock takes a blockbeat and processes it. It should not + // return until the subsystem has updated its state based on the block + // data. + // + // NOTE: The consumer must try its best to NOT return an error. If an + // error is returned from processing the block, it means the subsystem + // cannot react to onchain state changes and lnd will shutdown. + ProcessBlock(b Blockbeat) error +} diff --git a/chainio/log.go b/chainio/log.go new file mode 100644 index 00000000000..2d8c26f7a59 --- /dev/null +++ b/chainio/log.go @@ -0,0 +1,32 @@ +package chainio + +import ( + "github.com/btcsuite/btclog/v2" + "github.com/lightningnetwork/lnd/build" +) + +// Subsystem defines the logging code for this subsystem. +const Subsystem = "CHIO" + +// clog is a logger that is initialized with no output filters. This means the +// package will not perform any logging by default until the caller requests +// it. +var clog btclog.Logger + +// The default amount of logging is none. +func init() { + UseLogger(build.NewSubLogger(Subsystem, nil)) +} + +// DisableLog disables all library log output. Logging output is disabled by +// default until UseLogger is called. +func DisableLog() { + UseLogger(btclog.Disabled) +} + +// UseLogger uses a specified Logger to output package logging info. This +// should be used in preference to SetLogWriter if the caller is also using +// btclog. +func UseLogger(logger btclog.Logger) { + clog = logger +} From 01ac713aecbe4d428bf76008e175ef152275e8ca Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 27 Jun 2024 08:41:53 +0800 Subject: [PATCH 14/92] chainio: implement `Blockbeat` In this commit, a minimal implementation of `Blockbeat` is added to synchronize block heights, which will be used in `ChainArb`, `Sweeper`, and `TxPublisher` so blocks are processed sequentially among them. --- chainio/blockbeat.go | 54 +++++++++++++++++++++++++++++++++++++++ chainio/blockbeat_test.go | 28 ++++++++++++++++++++ chainio/interface.go | 19 +++++++++++--- 3 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 chainio/blockbeat.go create mode 100644 chainio/blockbeat_test.go diff --git a/chainio/blockbeat.go b/chainio/blockbeat.go new file mode 100644 index 00000000000..79188657fec --- /dev/null +++ b/chainio/blockbeat.go @@ -0,0 +1,54 @@ +package chainio + +import ( + "fmt" + + "github.com/btcsuite/btclog/v2" + "github.com/lightningnetwork/lnd/chainntnfs" +) + +// Beat implements the Blockbeat interface. It contains the block epoch and a +// customized logger. +// +// TODO(yy): extend this to check for confirmation status - which serves as the +// single source of truth, to avoid the potential race between receiving blocks +// and `GetTransactionDetails/RegisterSpendNtfn/RegisterConfirmationsNtfn`. +type Beat struct { + // epoch is the current block epoch the blockbeat is aware of. + epoch chainntnfs.BlockEpoch + + // log is the customized logger for the blockbeat which prints the + // block height. + log btclog.Logger +} + +// Compile-time check to ensure Beat satisfies the Blockbeat interface. +var _ Blockbeat = (*Beat)(nil) + +// NewBeat creates a new beat with the specified block epoch and a customized +// logger. +func NewBeat(epoch chainntnfs.BlockEpoch) *Beat { + b := &Beat{ + epoch: epoch, + } + + // Create a customized logger for the blockbeat. + logPrefix := fmt.Sprintf("Height[%6d]:", b.Height()) + b.log = clog.WithPrefix(logPrefix) + + return b +} + +// Height returns the height of the block epoch. +// +// NOTE: Part of the Blockbeat interface. +func (b *Beat) Height() int32 { + return b.epoch.Height +} + +// logger returns the logger for the blockbeat. +// +// NOTE: Part of the private blockbeat interface. +func (b *Beat) logger() btclog.Logger { + return b.log +} diff --git a/chainio/blockbeat_test.go b/chainio/blockbeat_test.go new file mode 100644 index 00000000000..9326651b38f --- /dev/null +++ b/chainio/blockbeat_test.go @@ -0,0 +1,28 @@ +package chainio + +import ( + "errors" + "testing" + + "github.com/lightningnetwork/lnd/chainntnfs" + "github.com/stretchr/testify/require" +) + +var errDummy = errors.New("dummy error") + +// TestNewBeat tests the NewBeat and Height functions. +func TestNewBeat(t *testing.T) { + t.Parallel() + + // Create a testing epoch. + epoch := chainntnfs.BlockEpoch{ + Height: 1, + } + + // Create the beat and check the internal state. + beat := NewBeat(epoch) + require.Equal(t, epoch, beat.epoch) + + // Check the height function. + require.Equal(t, epoch.Height, beat.Height()) +} diff --git a/chainio/interface.go b/chainio/interface.go index 70827f20763..03c09faf7c0 100644 --- a/chainio/interface.go +++ b/chainio/interface.go @@ -1,9 +1,11 @@ package chainio +import "github.com/btcsuite/btclog/v2" + // Blockbeat defines an interface that can be used by subsystems to retrieve -// block data. It is sent by the BlockbeatDispatcher whenever a new block is -// received. Once the subsystem finishes processing the block, it must signal -// it by calling NotifyBlockProcessed. +// block data. It is sent by the BlockbeatDispatcher to all the registered +// consumers whenever a new block is received. Once the consumer finishes +// processing the block, it must signal it by calling `NotifyBlockProcessed`. // // The blockchain is a state machine - whenever there's a state change, it's // manifested in a block. The blockbeat is a way to notify subsystems of this @@ -11,10 +13,21 @@ package chainio // other words, subsystems must react to this state change and should consider // being driven by the blockbeat in their own state machines. type Blockbeat interface { + // blockbeat is a private interface that's only used in this package. + blockbeat + // Height returns the current block height. Height() int32 } +// blockbeat defines a set of private methods used in this package to make +// interaction with the blockbeat easier. +type blockbeat interface { + // logger returns the internal logger used by the blockbeat which has a + // block height prefix. + logger() btclog.Logger +} + // Consumer defines a blockbeat consumer interface. Subsystems that need block // info must implement it. type Consumer interface { From a1eb87e2807548226a247719edb67b54b94c2070 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 1 Nov 2024 06:15:34 +0800 Subject: [PATCH 15/92] chainio: add helper methods to dispatch beats This commit adds two methods to handle dispatching beats. These are exported methods so other systems can send beats to their managed subinstances. --- chainio/dispatcher.go | 105 ++++++++++++++++++++++++ chainio/dispatcher_test.go | 161 +++++++++++++++++++++++++++++++++++++ chainio/mocks.go | 50 ++++++++++++ 3 files changed, 316 insertions(+) create mode 100644 chainio/dispatcher.go create mode 100644 chainio/dispatcher_test.go create mode 100644 chainio/mocks.go diff --git a/chainio/dispatcher.go b/chainio/dispatcher.go new file mode 100644 index 00000000000..d6900b8f9c1 --- /dev/null +++ b/chainio/dispatcher.go @@ -0,0 +1,105 @@ +package chainio + +import ( + "errors" + "fmt" + "time" +) + +// DefaultProcessBlockTimeout is the timeout value used when waiting for one +// consumer to finish processing the new block epoch. +var DefaultProcessBlockTimeout = 60 * time.Second + +// ErrProcessBlockTimeout is the error returned when a consumer takes too long +// to process the block. +var ErrProcessBlockTimeout = errors.New("process block timeout") + +// DispatchSequential takes a list of consumers and notify them about the new +// epoch sequentially. It requires the consumer to finish processing the block +// within the specified time, otherwise a timeout error is returned. +func DispatchSequential(b Blockbeat, consumers []Consumer) error { + for _, c := range consumers { + // Send the beat to the consumer. + err := notifyAndWait(b, c, DefaultProcessBlockTimeout) + if err != nil { + b.logger().Errorf("Failed to process block: %v", err) + + return err + } + } + + return nil +} + +// DispatchConcurrent notifies each consumer concurrently about the blockbeat. +// It requires the consumer to finish processing the block within the specified +// time, otherwise a timeout error is returned. +func DispatchConcurrent(b Blockbeat, consumers []Consumer) error { + // errChans is a map of channels that will be used to receive errors + // returned from notifying the consumers. + errChans := make(map[string]chan error, len(consumers)) + + // Notify each queue in goroutines. + for _, c := range consumers { + // Create a signal chan. + errChan := make(chan error, 1) + errChans[c.Name()] = errChan + + // Notify each consumer concurrently. + go func(c Consumer, beat Blockbeat) { + // Send the beat to the consumer. + errChan <- notifyAndWait( + b, c, DefaultProcessBlockTimeout, + ) + }(c, b) + } + + // Wait for all consumers in each queue to finish. + for name, errChan := range errChans { + err := <-errChan + if err != nil { + b.logger().Errorf("Consumer=%v failed to process "+ + "block: %v", name, err) + + return err + } + } + + return nil +} + +// notifyAndWait sends the blockbeat to the specified consumer. It requires the +// consumer to finish processing the block within the specified time, otherwise +// a timeout error is returned. +func notifyAndWait(b Blockbeat, c Consumer, timeout time.Duration) error { + b.logger().Debugf("Waiting for consumer[%s] to process it", c.Name()) + + // Record the time it takes the consumer to process this block. + start := time.Now() + + errChan := make(chan error, 1) + go func() { + errChan <- c.ProcessBlock(b) + }() + + // We expect the consumer to finish processing this block under 30s, + // otherwise a timeout error is returned. + select { + case err := <-errChan: + if err == nil { + break + } + + return fmt.Errorf("%s got err in ProcessBlock: %w", c.Name(), + err) + + case <-time.After(timeout): + return fmt.Errorf("consumer %s: %w", c.Name(), + ErrProcessBlockTimeout) + } + + b.logger().Debugf("Consumer[%s] processed block in %v", c.Name(), + time.Since(start)) + + return nil +} diff --git a/chainio/dispatcher_test.go b/chainio/dispatcher_test.go new file mode 100644 index 00000000000..c41138fd28c --- /dev/null +++ b/chainio/dispatcher_test.go @@ -0,0 +1,161 @@ +package chainio + +import ( + "testing" + "time" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +// TestNotifyAndWaitOnConsumerErr asserts when the consumer returns an error, +// it's returned by notifyAndWait. +func TestNotifyAndWaitOnConsumerErr(t *testing.T) { + t.Parallel() + + // Create a mock consumer. + consumer := &MockConsumer{} + defer consumer.AssertExpectations(t) + consumer.On("Name").Return("mocker") + + // Create a mock beat. + mockBeat := &MockBlockbeat{} + defer mockBeat.AssertExpectations(t) + mockBeat.On("logger").Return(clog) + + // Mock ProcessBlock to return an error. + consumer.On("ProcessBlock", mockBeat).Return(errDummy).Once() + + // Call the method under test. + err := notifyAndWait(mockBeat, consumer, DefaultProcessBlockTimeout) + + // We expect the error to be returned. + require.ErrorIs(t, err, errDummy) +} + +// TestNotifyAndWaitOnConsumerErr asserts when the consumer successfully +// processed the beat, no error is returned. +func TestNotifyAndWaitOnConsumerSuccess(t *testing.T) { + t.Parallel() + + // Create a mock consumer. + consumer := &MockConsumer{} + defer consumer.AssertExpectations(t) + consumer.On("Name").Return("mocker") + + // Create a mock beat. + mockBeat := &MockBlockbeat{} + defer mockBeat.AssertExpectations(t) + mockBeat.On("logger").Return(clog) + + // Mock ProcessBlock to return nil. + consumer.On("ProcessBlock", mockBeat).Return(nil).Once() + + // Call the method under test. + err := notifyAndWait(mockBeat, consumer, DefaultProcessBlockTimeout) + + // We expect a nil error to be returned. + require.NoError(t, err) +} + +// TestNotifyAndWaitOnConsumerTimeout asserts when the consumer times out +// processing the block, the timeout error is returned. +func TestNotifyAndWaitOnConsumerTimeout(t *testing.T) { + t.Parallel() + + // Set timeout to be 10ms. + processBlockTimeout := 10 * time.Millisecond + + // Create a mock consumer. + consumer := &MockConsumer{} + defer consumer.AssertExpectations(t) + consumer.On("Name").Return("mocker") + + // Create a mock beat. + mockBeat := &MockBlockbeat{} + defer mockBeat.AssertExpectations(t) + mockBeat.On("logger").Return(clog) + + // Mock ProcessBlock to return nil but blocks on returning. + consumer.On("ProcessBlock", mockBeat).Return(nil).Run( + func(args mock.Arguments) { + // Sleep one second to block on the method. + time.Sleep(processBlockTimeout * 100) + }).Once() + + // Call the method under test. + err := notifyAndWait(mockBeat, consumer, processBlockTimeout) + + // We expect a timeout error to be returned. + require.ErrorIs(t, err, ErrProcessBlockTimeout) +} + +// TestDispatchSequential checks that the beat is sent to the consumers +// sequentially. +func TestDispatchSequential(t *testing.T) { + t.Parallel() + + // Create three mock consumers. + consumer1 := &MockConsumer{} + defer consumer1.AssertExpectations(t) + consumer1.On("Name").Return("mocker1") + + consumer2 := &MockConsumer{} + defer consumer2.AssertExpectations(t) + consumer2.On("Name").Return("mocker2") + + consumer3 := &MockConsumer{} + defer consumer3.AssertExpectations(t) + consumer3.On("Name").Return("mocker3") + + consumers := []Consumer{consumer1, consumer2, consumer3} + + // Create a mock beat. + mockBeat := &MockBlockbeat{} + defer mockBeat.AssertExpectations(t) + mockBeat.On("logger").Return(clog) + + // prevConsumer specifies the previous consumer that was called. + var prevConsumer string + + // Mock the ProcessBlock on consumers to reutrn immediately. + consumer1.On("ProcessBlock", mockBeat).Return(nil).Run( + func(args mock.Arguments) { + // Check the order of the consumers. + // + // The first consumer should have no previous consumer. + require.Empty(t, prevConsumer) + + // Set the consumer as the previous consumer. + prevConsumer = consumer1.Name() + }).Once() + + consumer2.On("ProcessBlock", mockBeat).Return(nil).Run( + func(args mock.Arguments) { + // Check the order of the consumers. + // + // The second consumer should see consumer1. + require.Equal(t, consumer1.Name(), prevConsumer) + + // Set the consumer as the previous consumer. + prevConsumer = consumer2.Name() + }).Once() + + consumer3.On("ProcessBlock", mockBeat).Return(nil).Run( + func(args mock.Arguments) { + // Check the order of the consumers. + // + // The third consumer should see consumer2. + require.Equal(t, consumer2.Name(), prevConsumer) + + // Set the consumer as the previous consumer. + prevConsumer = consumer3.Name() + }).Once() + + // Call the method under test. + err := DispatchSequential(mockBeat, consumers) + require.NoError(t, err) + + // Check the previous consumer is the last consumer. + require.Equal(t, consumer3.Name(), prevConsumer) +} diff --git a/chainio/mocks.go b/chainio/mocks.go new file mode 100644 index 00000000000..5677734e1dd --- /dev/null +++ b/chainio/mocks.go @@ -0,0 +1,50 @@ +package chainio + +import ( + "github.com/btcsuite/btclog/v2" + "github.com/stretchr/testify/mock" +) + +// MockConsumer is a mock implementation of the Consumer interface. +type MockConsumer struct { + mock.Mock +} + +// Compile-time constraint to ensure MockConsumer implements Consumer. +var _ Consumer = (*MockConsumer)(nil) + +// Name returns a human-readable string for this subsystem. +func (m *MockConsumer) Name() string { + args := m.Called() + return args.String(0) +} + +// ProcessBlock takes a blockbeat and processes it. A receive-only error chan +// must be returned. +func (m *MockConsumer) ProcessBlock(b Blockbeat) error { + args := m.Called(b) + + return args.Error(0) +} + +// MockBlockbeat is a mock implementation of the Blockbeat interface. +type MockBlockbeat struct { + mock.Mock +} + +// Compile-time constraint to ensure MockBlockbeat implements Blockbeat. +var _ Blockbeat = (*MockBlockbeat)(nil) + +// Height returns the current block height. +func (m *MockBlockbeat) Height() int32 { + args := m.Called() + + return args.Get(0).(int32) +} + +// logger returns the logger for the blockbeat. +func (m *MockBlockbeat) logger() btclog.Logger { + args := m.Called() + + return args.Get(0).(btclog.Logger) +} From 4b83d87baa7c9f18c8c009cab7f3e86374993c39 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 27 Jun 2024 08:43:26 +0800 Subject: [PATCH 16/92] chainio: add `BlockbeatDispatcher` to dispatch blockbeats This commit adds a blockbeat dispatcher which handles sending new blocks to all subscribed consumers. --- chainio/dispatcher.go | 194 ++++++++++++++++++++++++++++++++ chainio/dispatcher_test.go | 222 +++++++++++++++++++++++++++++++++++++ 2 files changed, 416 insertions(+) diff --git a/chainio/dispatcher.go b/chainio/dispatcher.go index d6900b8f9c1..244a3ac8f73 100644 --- a/chainio/dispatcher.go +++ b/chainio/dispatcher.go @@ -3,7 +3,12 @@ package chainio import ( "errors" "fmt" + "sync" + "sync/atomic" "time" + + "github.com/btcsuite/btclog/v2" + "github.com/lightningnetwork/lnd/chainntnfs" ) // DefaultProcessBlockTimeout is the timeout value used when waiting for one @@ -14,6 +19,195 @@ var DefaultProcessBlockTimeout = 60 * time.Second // to process the block. var ErrProcessBlockTimeout = errors.New("process block timeout") +// BlockbeatDispatcher is a service that handles dispatching new blocks to +// `lnd`'s subsystems. During startup, subsystems that are block-driven should +// implement the `Consumer` interface and register themselves via +// `RegisterQueue`. When two subsystems are independent of each other, they +// should be registered in different queues so blocks are notified concurrently. +// Otherwise, when living in the same queue, the subsystems are notified of the +// new blocks sequentially, which means it's critical to understand the +// relationship of these systems to properly handle the order. +type BlockbeatDispatcher struct { + wg sync.WaitGroup + + // notifier is used to receive new block epochs. + notifier chainntnfs.ChainNotifier + + // beat is the latest blockbeat received. + beat Blockbeat + + // consumerQueues is a map of consumers that will receive blocks. Its + // key is a unique counter and its value is a queue of consumers. Each + // queue is notified concurrently, and consumers in the same queue is + // notified sequentially. + consumerQueues map[uint32][]Consumer + + // counter is used to assign a unique id to each queue. + counter atomic.Uint32 + + // quit is used to signal the BlockbeatDispatcher to stop. + quit chan struct{} +} + +// NewBlockbeatDispatcher returns a new blockbeat dispatcher instance. +func NewBlockbeatDispatcher(n chainntnfs.ChainNotifier) *BlockbeatDispatcher { + return &BlockbeatDispatcher{ + notifier: n, + quit: make(chan struct{}), + consumerQueues: make(map[uint32][]Consumer), + } +} + +// RegisterQueue takes a list of consumers and registers them in the same +// queue. +// +// NOTE: these consumers are notified sequentially. +func (b *BlockbeatDispatcher) RegisterQueue(consumers []Consumer) { + qid := b.counter.Add(1) + + b.consumerQueues[qid] = append(b.consumerQueues[qid], consumers...) + clog.Infof("Registered queue=%d with %d blockbeat consumers", qid, + len(consumers)) + + for _, c := range consumers { + clog.Debugf("Consumer [%s] registered in queue %d", c.Name(), + qid) + } +} + +// Start starts the blockbeat dispatcher - it registers a block notification +// and monitors and dispatches new blocks in a goroutine. It will refuse to +// start if there are no registered consumers. +func (b *BlockbeatDispatcher) Start() error { + // Make sure consumers are registered. + if len(b.consumerQueues) == 0 { + return fmt.Errorf("no consumers registered") + } + + // Start listening to new block epochs. We should get a notification + // with the current best block immediately. + blockEpochs, err := b.notifier.RegisterBlockEpochNtfn(nil) + if err != nil { + return fmt.Errorf("register block epoch ntfn: %w", err) + } + + clog.Infof("BlockbeatDispatcher is starting with %d consumer queues", + len(b.consumerQueues)) + defer clog.Debug("BlockbeatDispatcher started") + + b.wg.Add(1) + go b.dispatchBlocks(blockEpochs) + + return nil +} + +// Stop shuts down the blockbeat dispatcher. +func (b *BlockbeatDispatcher) Stop() { + clog.Info("BlockbeatDispatcher is stopping") + defer clog.Debug("BlockbeatDispatcher stopped") + + // Signal the dispatchBlocks goroutine to stop. + close(b.quit) + b.wg.Wait() +} + +func (b *BlockbeatDispatcher) log() btclog.Logger { + return b.beat.logger() +} + +// dispatchBlocks listens to new block epoch and dispatches it to all the +// consumers. Each queue is notified concurrently, and the consumers in the +// same queue are notified sequentially. +// +// NOTE: Must be run as a goroutine. +func (b *BlockbeatDispatcher) dispatchBlocks( + blockEpochs *chainntnfs.BlockEpochEvent) { + + defer b.wg.Done() + defer blockEpochs.Cancel() + + for { + select { + case blockEpoch, ok := <-blockEpochs.Epochs: + if !ok { + clog.Debugf("Block epoch channel closed") + + return + } + + clog.Infof("Received new block %v at height %d, "+ + "notifying consumers...", blockEpoch.Hash, + blockEpoch.Height) + + // Record the time it takes the consumer to process + // this block. + start := time.Now() + + // Update the current block epoch. + b.beat = NewBeat(*blockEpoch) + + // Notify all consumers. + err := b.notifyQueues() + if err != nil { + b.log().Errorf("Notify block failed: %v", err) + } + + b.log().Infof("Notified all consumers on new block "+ + "in %v", time.Since(start)) + + case <-b.quit: + b.log().Debugf("BlockbeatDispatcher quit signal " + + "received") + + return + } + } +} + +// notifyQueues notifies each queue concurrently about the latest block epoch. +func (b *BlockbeatDispatcher) notifyQueues() error { + // errChans is a map of channels that will be used to receive errors + // returned from notifying the consumers. + errChans := make(map[uint32]chan error, len(b.consumerQueues)) + + // Notify each queue in goroutines. + for qid, consumers := range b.consumerQueues { + b.log().Debugf("Notifying queue=%d with %d consumers", qid, + len(consumers)) + + // Create a signal chan. + errChan := make(chan error, 1) + errChans[qid] = errChan + + // Notify each queue concurrently. + go func(qid uint32, c []Consumer, beat Blockbeat) { + // Notify each consumer in this queue sequentially. + errChan <- DispatchSequential(beat, c) + }(qid, consumers, b.beat) + } + + // Wait for all consumers in each queue to finish. + for qid, errChan := range errChans { + select { + case err := <-errChan: + if err != nil { + return fmt.Errorf("queue=%d got err: %w", qid, + err) + } + + b.log().Debugf("Notified queue=%d", qid) + + case <-b.quit: + b.log().Debugf("BlockbeatDispatcher quit signal " + + "received, exit notifyQueues") + + return nil + } + } + + return nil +} + // DispatchSequential takes a list of consumers and notify them about the new // epoch sequentially. It requires the consumer to finish processing the block // within the specified time, otherwise a timeout error is returned. diff --git a/chainio/dispatcher_test.go b/chainio/dispatcher_test.go index c41138fd28c..11abbeb65e0 100644 --- a/chainio/dispatcher_test.go +++ b/chainio/dispatcher_test.go @@ -4,6 +4,8 @@ import ( "testing" "time" + "github.com/lightningnetwork/lnd/chainntnfs" + "github.com/lightningnetwork/lnd/fn/v2" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -159,3 +161,223 @@ func TestDispatchSequential(t *testing.T) { // Check the previous consumer is the last consumer. require.Equal(t, consumer3.Name(), prevConsumer) } + +// TestRegisterQueue tests the RegisterQueue function. +func TestRegisterQueue(t *testing.T) { + t.Parallel() + + // Create two mock consumers. + consumer1 := &MockConsumer{} + defer consumer1.AssertExpectations(t) + consumer1.On("Name").Return("mocker1") + + consumer2 := &MockConsumer{} + defer consumer2.AssertExpectations(t) + consumer2.On("Name").Return("mocker2") + + consumers := []Consumer{consumer1, consumer2} + + // Create a mock chain notifier. + mockNotifier := &chainntnfs.MockChainNotifier{} + defer mockNotifier.AssertExpectations(t) + + // Create a new dispatcher. + b := NewBlockbeatDispatcher(mockNotifier) + + // Register the consumers. + b.RegisterQueue(consumers) + + // Assert that the consumers have been registered. + // + // We should have one queue. + require.Len(t, b.consumerQueues, 1) + + // The queue should have two consumers. + queue, ok := b.consumerQueues[1] + require.True(t, ok) + require.Len(t, queue, 2) +} + +// TestStartDispatcher tests the Start method. +func TestStartDispatcher(t *testing.T) { + t.Parallel() + + // Create a mock chain notifier. + mockNotifier := &chainntnfs.MockChainNotifier{} + defer mockNotifier.AssertExpectations(t) + + // Create a new dispatcher. + b := NewBlockbeatDispatcher(mockNotifier) + + // Start the dispatcher without consumers should return an error. + err := b.Start() + require.Error(t, err) + + // Create a consumer and register it. + consumer := &MockConsumer{} + defer consumer.AssertExpectations(t) + consumer.On("Name").Return("mocker1") + b.RegisterQueue([]Consumer{consumer}) + + // Mock the chain notifier to return an error. + mockNotifier.On("RegisterBlockEpochNtfn", + mock.Anything).Return(nil, errDummy).Once() + + // Start the dispatcher now should return the error. + err = b.Start() + require.ErrorIs(t, err, errDummy) + + // Mock the chain notifier to return a valid notifier. + blockEpochs := &chainntnfs.BlockEpochEvent{} + mockNotifier.On("RegisterBlockEpochNtfn", + mock.Anything).Return(blockEpochs, nil).Once() + + // Start the dispatcher now should not return an error. + err = b.Start() + require.NoError(t, err) +} + +// TestDispatchBlocks asserts the blocks are properly dispatched to the queues. +func TestDispatchBlocks(t *testing.T) { + t.Parallel() + + // Create a mock chain notifier. + mockNotifier := &chainntnfs.MockChainNotifier{} + defer mockNotifier.AssertExpectations(t) + + // Create a new dispatcher. + b := NewBlockbeatDispatcher(mockNotifier) + + // Create the beat and attach it to the dispatcher. + epoch := chainntnfs.BlockEpoch{Height: 1} + beat := NewBeat(epoch) + b.beat = beat + + // Create a consumer and register it. + consumer := &MockConsumer{} + defer consumer.AssertExpectations(t) + consumer.On("Name").Return("mocker1") + b.RegisterQueue([]Consumer{consumer}) + + // Mock the consumer to return nil error on ProcessBlock. This + // implictly asserts that the step `notifyQueues` is successfully + // reached in the `dispatchBlocks` method. + consumer.On("ProcessBlock", mock.Anything).Return(nil).Once() + + // Create a test epoch chan. + epochChan := make(chan *chainntnfs.BlockEpoch, 1) + blockEpochs := &chainntnfs.BlockEpochEvent{ + Epochs: epochChan, + Cancel: func() {}, + } + + // Call the method in a goroutine. + done := make(chan struct{}) + b.wg.Add(1) + go func() { + defer close(done) + b.dispatchBlocks(blockEpochs) + }() + + // Send an epoch. + epoch = chainntnfs.BlockEpoch{Height: 2} + epochChan <- &epoch + + // Wait for the dispatcher to process the epoch. + time.Sleep(100 * time.Millisecond) + + // Stop the dispatcher. + b.Stop() + + // We expect the dispatcher to stop immediately. + _, err := fn.RecvOrTimeout(done, time.Second) + require.NoError(t, err) +} + +// TestNotifyQueuesSuccess checks when the dispatcher successfully notifies all +// the queues, no error is returned. +func TestNotifyQueuesSuccess(t *testing.T) { + t.Parallel() + + // Create two mock consumers. + consumer1 := &MockConsumer{} + defer consumer1.AssertExpectations(t) + consumer1.On("Name").Return("mocker1") + + consumer2 := &MockConsumer{} + defer consumer2.AssertExpectations(t) + consumer2.On("Name").Return("mocker2") + + // Create two queues. + queue1 := []Consumer{consumer1} + queue2 := []Consumer{consumer2} + + // Create a mock chain notifier. + mockNotifier := &chainntnfs.MockChainNotifier{} + defer mockNotifier.AssertExpectations(t) + + // Create a mock beat. + mockBeat := &MockBlockbeat{} + defer mockBeat.AssertExpectations(t) + mockBeat.On("logger").Return(clog) + + // Create a new dispatcher. + b := NewBlockbeatDispatcher(mockNotifier) + + // Register the queues. + b.RegisterQueue(queue1) + b.RegisterQueue(queue2) + + // Attach the blockbeat. + b.beat = mockBeat + + // Mock the consumers to return nil error on ProcessBlock for + // both calls. + consumer1.On("ProcessBlock", mockBeat).Return(nil).Once() + consumer2.On("ProcessBlock", mockBeat).Return(nil).Once() + + // Notify the queues. The mockers will be asserted in the end to + // validate the calls. + err := b.notifyQueues() + require.NoError(t, err) +} + +// TestNotifyQueuesError checks when one of the queue returns an error, this +// error is returned by the method. +func TestNotifyQueuesError(t *testing.T) { + t.Parallel() + + // Create a mock consumer. + consumer := &MockConsumer{} + defer consumer.AssertExpectations(t) + consumer.On("Name").Return("mocker1") + + // Create one queue. + queue := []Consumer{consumer} + + // Create a mock chain notifier. + mockNotifier := &chainntnfs.MockChainNotifier{} + defer mockNotifier.AssertExpectations(t) + + // Create a mock beat. + mockBeat := &MockBlockbeat{} + defer mockBeat.AssertExpectations(t) + mockBeat.On("logger").Return(clog) + + // Create a new dispatcher. + b := NewBlockbeatDispatcher(mockNotifier) + + // Register the queues. + b.RegisterQueue(queue) + + // Attach the blockbeat. + b.beat = mockBeat + + // Mock the consumer to return an error on ProcessBlock. + consumer.On("ProcessBlock", mockBeat).Return(errDummy).Once() + + // Notify the queues. The mockers will be asserted in the end to + // validate the calls. + err := b.notifyQueues() + require.ErrorIs(t, err, errDummy) +} From b5a3a27c77593018c49ee1478578a41ef0ee2d25 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 17 Oct 2024 10:58:59 +0800 Subject: [PATCH 17/92] chainio: add partial implementation of `Consumer` interface --- chainio/consumer.go | 113 ++++++++++++++++++++++ chainio/consumer_test.go | 202 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 315 insertions(+) create mode 100644 chainio/consumer.go create mode 100644 chainio/consumer_test.go diff --git a/chainio/consumer.go b/chainio/consumer.go new file mode 100644 index 00000000000..a9ec25745ba --- /dev/null +++ b/chainio/consumer.go @@ -0,0 +1,113 @@ +package chainio + +// BeatConsumer defines a supplementary component that should be used by +// subsystems which implement the `Consumer` interface. It partially implements +// the `Consumer` interface by providing the method `ProcessBlock` such that +// subsystems don't need to re-implement it. +// +// While inheritance is not commonly used in Go, subsystems embedding this +// struct cannot pass the interface check for `Consumer` because the `Name` +// method is not implemented, which gives us a "mortise and tenon" structure. +// In addition to reducing code duplication, this design allows `ProcessBlock` +// to work on the concrete type `Beat` to access its internal states. +type BeatConsumer struct { + // BlockbeatChan is a channel to receive blocks from Blockbeat. The + // received block contains the best known height and the txns confirmed + // in this block. + BlockbeatChan chan Blockbeat + + // name is the name of the consumer which embeds the BlockConsumer. + name string + + // quit is a channel that closes when the BlockConsumer is shutting + // down. + // + // NOTE: this quit channel should be mounted to the same quit channel + // used by the subsystem. + quit chan struct{} + + // errChan is a buffered chan that receives an error returned from + // processing this block. + errChan chan error +} + +// NewBeatConsumer creates a new BlockConsumer. +func NewBeatConsumer(quit chan struct{}, name string) BeatConsumer { + // Refuse to start `lnd` if the quit channel is not initialized. We + // treat this case as if we are facing a nil pointer dereference, as + // there's no point to return an error here, which will cause the node + // to fail to be started anyway. + if quit == nil { + panic("quit channel is nil") + } + + b := BeatConsumer{ + BlockbeatChan: make(chan Blockbeat), + name: name, + errChan: make(chan error, 1), + quit: quit, + } + + return b +} + +// ProcessBlock takes a blockbeat and sends it to the consumer's blockbeat +// channel. It will send it to the subsystem's BlockbeatChan, and block until +// the processed result is received from the subsystem. The subsystem must call +// `NotifyBlockProcessed` after it has finished processing the block. +// +// NOTE: part of the `chainio.Consumer` interface. +func (b *BeatConsumer) ProcessBlock(beat Blockbeat) error { + // Update the current height. + beat.logger().Tracef("set current height for [%s]", b.name) + + select { + // Send the beat to the blockbeat channel. It's expected that the + // consumer will read from this channel and process the block. Once + // processed, it should return the error or nil to the beat.Err chan. + case b.BlockbeatChan <- beat: + beat.logger().Tracef("Sent blockbeat to [%s]", b.name) + + case <-b.quit: + beat.logger().Debugf("[%s] received shutdown before sending "+ + "beat", b.name) + + return nil + } + + // Check the consumer's err chan. We expect the consumer to call + // `beat.NotifyBlockProcessed` to send the error back here. + select { + case err := <-b.errChan: + beat.logger().Debugf("[%s] processed beat: err=%v", b.name, err) + + return err + + case <-b.quit: + beat.logger().Debugf("[%s] received shutdown", b.name) + } + + return nil +} + +// NotifyBlockProcessed signals that the block has been processed. It takes the +// blockbeat being processed and an error resulted from processing it. This +// error is then sent back to the consumer's err chan to unblock +// `ProcessBlock`. +// +// NOTE: This method must be called by the subsystem after it has finished +// processing the block. +func (b *BeatConsumer) NotifyBlockProcessed(beat Blockbeat, err error) { + // Update the current height. + beat.logger().Debugf("[%s]: notifying beat processed", b.name) + + select { + case b.errChan <- err: + beat.logger().Debugf("[%s]: notified beat processed, err=%v", + b.name, err) + + case <-b.quit: + beat.logger().Debugf("[%s] received shutdown before notifying "+ + "beat processed", b.name) + } +} diff --git a/chainio/consumer_test.go b/chainio/consumer_test.go new file mode 100644 index 00000000000..d1cabf3168e --- /dev/null +++ b/chainio/consumer_test.go @@ -0,0 +1,202 @@ +package chainio + +import ( + "testing" + "time" + + "github.com/lightningnetwork/lnd/fn/v2" + "github.com/stretchr/testify/require" +) + +// TestNewBeatConsumer tests the NewBeatConsumer function. +func TestNewBeatConsumer(t *testing.T) { + t.Parallel() + + quitChan := make(chan struct{}) + name := "test" + + // Test the NewBeatConsumer function. + b := NewBeatConsumer(quitChan, name) + + // Assert the state. + require.Equal(t, quitChan, b.quit) + require.Equal(t, name, b.name) + require.NotNil(t, b.BlockbeatChan) +} + +// TestProcessBlockSuccess tests when the block is processed successfully, no +// error is returned. +func TestProcessBlockSuccess(t *testing.T) { + t.Parallel() + + // Create a test consumer. + quitChan := make(chan struct{}) + b := NewBeatConsumer(quitChan, "test") + + // Create a mock beat. + mockBeat := &MockBlockbeat{} + defer mockBeat.AssertExpectations(t) + mockBeat.On("logger").Return(clog) + + // Mock the consumer's err chan. + consumerErrChan := make(chan error, 1) + b.errChan = consumerErrChan + + // Call the method under test. + resultChan := make(chan error, 1) + go func() { + resultChan <- b.ProcessBlock(mockBeat) + }() + + // Assert the beat is sent to the blockbeat channel. + beat, err := fn.RecvOrTimeout(b.BlockbeatChan, time.Second) + require.NoError(t, err) + require.Equal(t, mockBeat, beat) + + // Send nil to the consumer's error channel. + consumerErrChan <- nil + + // Assert the result of ProcessBlock is nil. + result, err := fn.RecvOrTimeout(resultChan, time.Second) + require.NoError(t, err) + require.Nil(t, result) +} + +// TestProcessBlockConsumerQuitBeforeSend tests when the consumer is quit +// before sending the beat, the method returns immediately. +func TestProcessBlockConsumerQuitBeforeSend(t *testing.T) { + t.Parallel() + + // Create a test consumer. + quitChan := make(chan struct{}) + b := NewBeatConsumer(quitChan, "test") + + // Create a mock beat. + mockBeat := &MockBlockbeat{} + defer mockBeat.AssertExpectations(t) + mockBeat.On("logger").Return(clog) + + // Call the method under test. + resultChan := make(chan error, 1) + go func() { + resultChan <- b.ProcessBlock(mockBeat) + }() + + // Instead of reading the BlockbeatChan, close the quit channel. + close(quitChan) + + // Assert ProcessBlock returned nil. + result, err := fn.RecvOrTimeout(resultChan, time.Second) + require.NoError(t, err) + require.Nil(t, result) +} + +// TestProcessBlockConsumerQuitAfterSend tests when the consumer is quit after +// sending the beat, the method returns immediately. +func TestProcessBlockConsumerQuitAfterSend(t *testing.T) { + t.Parallel() + + // Create a test consumer. + quitChan := make(chan struct{}) + b := NewBeatConsumer(quitChan, "test") + + // Create a mock beat. + mockBeat := &MockBlockbeat{} + defer mockBeat.AssertExpectations(t) + mockBeat.On("logger").Return(clog) + + // Mock the consumer's err chan. + consumerErrChan := make(chan error, 1) + b.errChan = consumerErrChan + + // Call the method under test. + resultChan := make(chan error, 1) + go func() { + resultChan <- b.ProcessBlock(mockBeat) + }() + + // Assert the beat is sent to the blockbeat channel. + beat, err := fn.RecvOrTimeout(b.BlockbeatChan, time.Second) + require.NoError(t, err) + require.Equal(t, mockBeat, beat) + + // Instead of sending nil to the consumer's error channel, close the + // quit chanel. + close(quitChan) + + // Assert ProcessBlock returned nil. + result, err := fn.RecvOrTimeout(resultChan, time.Second) + require.NoError(t, err) + require.Nil(t, result) +} + +// TestNotifyBlockProcessedSendErr asserts the error can be sent and read by +// the beat via NotifyBlockProcessed. +func TestNotifyBlockProcessedSendErr(t *testing.T) { + t.Parallel() + + // Create a test consumer. + quitChan := make(chan struct{}) + b := NewBeatConsumer(quitChan, "test") + + // Create a mock beat. + mockBeat := &MockBlockbeat{} + defer mockBeat.AssertExpectations(t) + mockBeat.On("logger").Return(clog) + + // Mock the consumer's err chan. + consumerErrChan := make(chan error, 1) + b.errChan = consumerErrChan + + // Call the method under test. + done := make(chan error) + go func() { + defer close(done) + b.NotifyBlockProcessed(mockBeat, errDummy) + }() + + // Assert the error is sent to the beat's err chan. + result, err := fn.RecvOrTimeout(consumerErrChan, time.Second) + require.NoError(t, err) + require.ErrorIs(t, result, errDummy) + + // Assert the done channel is closed. + result, err = fn.RecvOrTimeout(done, time.Second) + require.NoError(t, err) + require.Nil(t, result) +} + +// TestNotifyBlockProcessedOnQuit asserts NotifyBlockProcessed exits +// immediately when the quit channel is closed. +func TestNotifyBlockProcessedOnQuit(t *testing.T) { + t.Parallel() + + // Create a test consumer. + quitChan := make(chan struct{}) + b := NewBeatConsumer(quitChan, "test") + + // Create a mock beat. + mockBeat := &MockBlockbeat{} + defer mockBeat.AssertExpectations(t) + mockBeat.On("logger").Return(clog) + + // Mock the consumer's err chan - we don't buffer it so it will block + // on sending the error. + consumerErrChan := make(chan error) + b.errChan = consumerErrChan + + // Call the method under test. + done := make(chan error) + go func() { + defer close(done) + b.NotifyBlockProcessed(mockBeat, errDummy) + }() + + // Close the quit channel so the method will return. + close(b.quit) + + // Assert the done channel is closed. + result, err := fn.RecvOrTimeout(done, time.Second) + require.NoError(t, err) + require.Nil(t, result) +} From 801fd6b85bc036ce69995403e1f250e194f66161 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 29 Oct 2024 21:23:29 +0800 Subject: [PATCH 18/92] multi: implement `Consumer` on subsystems This commit implements `Consumer` on `TxPublisher`, `UtxoSweeper`, `ChainArbitrator` and `ChannelArbitrator`. --- contractcourt/chain_arbitrator.go | 20 +++++++++++++++++++- contractcourt/channel_arbitrator.go | 22 +++++++++++++++++++++- sweep/fee_bumper.go | 20 +++++++++++++++++++- sweep/sweeper.go | 20 +++++++++++++++++++- 4 files changed, 78 insertions(+), 4 deletions(-) diff --git a/contractcourt/chain_arbitrator.go b/contractcourt/chain_arbitrator.go index 646d68b869e..e8ba2907aa5 100644 --- a/contractcourt/chain_arbitrator.go +++ b/contractcourt/chain_arbitrator.go @@ -11,6 +11,7 @@ import ( "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcwallet/walletdb" + "github.com/lightningnetwork/lnd/chainio" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/clock" @@ -244,6 +245,10 @@ type ChainArbitrator struct { started int32 // To be used atomically. stopped int32 // To be used atomically. + // Embed the blockbeat consumer struct to get access to the method + // `NotifyBlockProcessed` and the `BlockbeatChan`. + chainio.BeatConsumer + sync.Mutex // activeChannels is a map of all the active contracts that are still @@ -272,15 +277,23 @@ type ChainArbitrator struct { func NewChainArbitrator(cfg ChainArbitratorConfig, db *channeldb.DB) *ChainArbitrator { - return &ChainArbitrator{ + c := &ChainArbitrator{ cfg: cfg, activeChannels: make(map[wire.OutPoint]*ChannelArbitrator), activeWatchers: make(map[wire.OutPoint]*chainWatcher), chanSource: db, quit: make(chan struct{}), } + + // Mount the block consumer. + c.BeatConsumer = chainio.NewBeatConsumer(c.quit, c.Name()) + + return c } +// Compile-time check for the chainio.Consumer interface. +var _ chainio.Consumer = (*ChainArbitrator)(nil) + // arbChannel is a wrapper around an open channel that channel arbitrators // interact with. type arbChannel struct { @@ -1361,3 +1374,8 @@ func (c *ChainArbitrator) FindOutgoingHTLCDeadline(scid lnwire.ShortChannelID, // TODO(roasbeef): arbitration reports // * types: contested, waiting for success conf, etc + +// NOTE: part of the `chainio.Consumer` interface. +func (c *ChainArbitrator) Name() string { + return "ChainArbitrator" +} diff --git a/contractcourt/channel_arbitrator.go b/contractcourt/channel_arbitrator.go index 3404207b629..0c8dde532ec 100644 --- a/contractcourt/channel_arbitrator.go +++ b/contractcourt/channel_arbitrator.go @@ -14,6 +14,7 @@ import ( "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/chainio" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/graph/db/models" @@ -330,6 +331,10 @@ type ChannelArbitrator struct { started int32 // To be used atomically. stopped int32 // To be used atomically. + // Embed the blockbeat consumer struct to get access to the method + // `NotifyBlockProcessed` and the `BlockbeatChan`. + chainio.BeatConsumer + // startTimestamp is the time when this ChannelArbitrator was started. startTimestamp time.Time @@ -404,7 +409,7 @@ func NewChannelArbitrator(cfg ChannelArbitratorConfig, unmerged[RemotePendingHtlcSet] = htlcSets[RemotePendingHtlcSet] } - return &ChannelArbitrator{ + c := &ChannelArbitrator{ log: log, blocks: make(chan int32, arbitratorBlockBufferSize), signalUpdates: make(chan *signalUpdateMsg), @@ -415,8 +420,16 @@ func NewChannelArbitrator(cfg ChannelArbitratorConfig, cfg: cfg, quit: make(chan struct{}), } + + // Mount the block consumer. + c.BeatConsumer = chainio.NewBeatConsumer(c.quit, c.Name()) + + return c } +// Compile-time check for the chainio.Consumer interface. +var _ chainio.Consumer = (*ChannelArbitrator)(nil) + // chanArbStartState contains the information from disk that we need to start // up a channel arbitrator. type chanArbStartState struct { @@ -3131,6 +3144,13 @@ func (c *ChannelArbitrator) channelAttendant(bestHeight int32, } } +// Name returns a human-readable string for this subsystem. +// +// NOTE: Part of chainio.Consumer interface. +func (c *ChannelArbitrator) Name() string { + return fmt.Sprintf("ChannelArbitrator(%v)", c.cfg.ChanPoint) +} + // checkLegacyBreach returns StateFullyResolved if the channel was closed with // a breach transaction before the channel arbitrator launched its own breach // resolver. StateContractClosed is returned if this is a modern breach close diff --git a/sweep/fee_bumper.go b/sweep/fee_bumper.go index 13fa1272c68..3c973eb1190 100644 --- a/sweep/fee_bumper.go +++ b/sweep/fee_bumper.go @@ -12,6 +12,7 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcwallet/chain" + "github.com/lightningnetwork/lnd/chainio" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/input" @@ -344,6 +345,10 @@ type TxPublisher struct { started atomic.Bool stopped atomic.Bool + // Embed the blockbeat consumer struct to get access to the method + // `NotifyBlockProcessed` and the `BlockbeatChan`. + chainio.BeatConsumer + wg sync.WaitGroup // cfg specifies the configuration of the TxPublisher. @@ -371,14 +376,22 @@ type TxPublisher struct { // Compile-time constraint to ensure TxPublisher implements Bumper. var _ Bumper = (*TxPublisher)(nil) +// Compile-time check for the chainio.Consumer interface. +var _ chainio.Consumer = (*TxPublisher)(nil) + // NewTxPublisher creates a new TxPublisher. func NewTxPublisher(cfg TxPublisherConfig) *TxPublisher { - return &TxPublisher{ + tp := &TxPublisher{ cfg: &cfg, records: lnutils.SyncMap[uint64, *monitorRecord]{}, subscriberChans: lnutils.SyncMap[uint64, chan *BumpResult]{}, quit: make(chan struct{}), } + + // Mount the block consumer. + tp.BeatConsumer = chainio.NewBeatConsumer(tp.quit, tp.Name()) + + return tp } // isNeutrinoBackend checks if the wallet backend is neutrino. @@ -427,6 +440,11 @@ func (t *TxPublisher) storeInitialRecord(req *BumpRequest) ( return requestID, record } +// NOTE: part of the `chainio.Consumer` interface. +func (t *TxPublisher) Name() string { + return "TxPublisher" +} + // initializeTx initializes a fee function and creates an RBF-compliant tx. If // succeeded, the initial tx is stored in the records map. func (t *TxPublisher) initializeTx(requestID uint64, req *BumpRequest) error { diff --git a/sweep/sweeper.go b/sweep/sweeper.go index 976fceff314..9b06c05495a 100644 --- a/sweep/sweeper.go +++ b/sweep/sweeper.go @@ -10,6 +10,7 @@ import ( "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/davecgh/go-spew/spew" + "github.com/lightningnetwork/lnd/chainio" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/input" @@ -308,6 +309,10 @@ type UtxoSweeper struct { started uint32 // To be used atomically. stopped uint32 // To be used atomically. + // Embed the blockbeat consumer struct to get access to the method + // `NotifyBlockProcessed` and the `BlockbeatChan`. + chainio.BeatConsumer + cfg *UtxoSweeperConfig newInputs chan *sweepInputMessage @@ -342,6 +347,9 @@ type UtxoSweeper struct { bumpRespChan chan *bumpResp } +// Compile-time check for the chainio.Consumer interface. +var _ chainio.Consumer = (*UtxoSweeper)(nil) + // UtxoSweeperConfig contains dependencies of UtxoSweeper. type UtxoSweeperConfig struct { // GenSweepScript generates a P2WKH script belonging to the wallet where @@ -415,7 +423,7 @@ type sweepInputMessage struct { // New returns a new Sweeper instance. func New(cfg *UtxoSweeperConfig) *UtxoSweeper { - return &UtxoSweeper{ + s := &UtxoSweeper{ cfg: cfg, newInputs: make(chan *sweepInputMessage), spendChan: make(chan *chainntnfs.SpendDetail), @@ -425,6 +433,11 @@ func New(cfg *UtxoSweeperConfig) *UtxoSweeper { inputs: make(InputsMap), bumpRespChan: make(chan *bumpResp, 100), } + + // Mount the block consumer. + s.BeatConsumer = chainio.NewBeatConsumer(s.quit, s.Name()) + + return s } // Start starts the process of constructing and publish sweep txes. @@ -508,6 +521,11 @@ func (s *UtxoSweeper) Stop() error { return nil } +// NOTE: part of the `chainio.Consumer` interface. +func (s *UtxoSweeper) Name() string { + return "UtxoSweeper" +} + // SweepInput sweeps inputs back into the wallet. The inputs will be batched and // swept after the batch time window ends. A custom fee preference can be // provided to determine what fee rate should be used for the input. Note that From e113f39d268726a9781b329a78aa2067600eb707 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 4 Jun 2024 20:31:03 +0800 Subject: [PATCH 19/92] sweep: remove block subscription in `UtxoSweeper` and `TxPublisher` This commit removes the independent block subscriptions in `UtxoSweeper` and `TxPublisher`. These subsystems now listen to the `BlockbeatChan` for new blocks. --- sweep/fee_bumper.go | 31 +++++++++---------------------- sweep/sweeper.go | 42 +++++++++--------------------------------- 2 files changed, 18 insertions(+), 55 deletions(-) diff --git a/sweep/fee_bumper.go b/sweep/fee_bumper.go index 3c973eb1190..ad02ac61946 100644 --- a/sweep/fee_bumper.go +++ b/sweep/fee_bumper.go @@ -802,13 +802,8 @@ func (t *TxPublisher) Start() error { return fmt.Errorf("TxPublisher started more than once") } - blockEvent, err := t.cfg.Notifier.RegisterBlockEpochNtfn(nil) - if err != nil { - return fmt.Errorf("register block epoch ntfn: %w", err) - } - t.wg.Add(1) - go t.monitor(blockEvent) + go t.monitor() log.Debugf("TxPublisher started") @@ -836,33 +831,25 @@ func (t *TxPublisher) Stop() error { // to be bumped. If so, it will attempt to bump the fee of the tx. // // NOTE: Must be run as a goroutine. -func (t *TxPublisher) monitor(blockEvent *chainntnfs.BlockEpochEvent) { - defer blockEvent.Cancel() +func (t *TxPublisher) monitor() { defer t.wg.Done() for { select { - case epoch, ok := <-blockEvent.Epochs: - if !ok { - // We should stop the publisher before stopping - // the chain service. Otherwise it indicates an - // error. - log.Error("Block epoch channel closed, exit " + - "monitor") - - return - } - - log.Debugf("TxPublisher received new block: %v", - epoch.Height) + case beat := <-t.BlockbeatChan: + height := beat.Height() + log.Debugf("TxPublisher received new block: %v", height) // Update the best known height for the publisher. - t.currentHeight.Store(epoch.Height) + t.currentHeight.Store(height) // Check all monitored txns to see if any of them needs // to be bumped. t.processRecords() + // Notify we've processed the block. + t.NotifyBlockProcessed(beat, nil) + case <-t.quit: log.Debug("Fee bumper stopped, exit monitor") return diff --git a/sweep/sweeper.go b/sweep/sweeper.go index 9b06c05495a..64306935c9b 100644 --- a/sweep/sweeper.go +++ b/sweep/sweeper.go @@ -452,21 +452,12 @@ func (s *UtxoSweeper) Start() error { // not change from here on. s.relayFeeRate = s.cfg.FeeEstimator.RelayFeePerKW() - // We need to register for block epochs and retry sweeping every block. - // We should get a notification with the current best block immediately - // if we don't provide any epoch. We'll wait for that in the collector. - blockEpochs, err := s.cfg.Notifier.RegisterBlockEpochNtfn(nil) - if err != nil { - return fmt.Errorf("register block epoch ntfn: %w", err) - } - // Start sweeper main loop. s.wg.Add(1) go func() { - defer blockEpochs.Cancel() defer s.wg.Done() - s.collector(blockEpochs.Epochs) + s.collector() // The collector exited and won't longer handle incoming // requests. This can happen on shutdown, when the block @@ -657,17 +648,8 @@ func (s *UtxoSweeper) removeConflictSweepDescendants( // collector is the sweeper main loop. It processes new inputs, spend // notifications and counts down to publication of the sweep tx. -func (s *UtxoSweeper) collector(blockEpochs <-chan *chainntnfs.BlockEpoch) { - // We registered for the block epochs with a nil request. The notifier - // should send us the current best block immediately. So we need to wait - // for it here because we need to know the current best height. - select { - case bestBlock := <-blockEpochs: - s.currentHeight = bestBlock.Height - - case <-s.quit: - return - } +func (s *UtxoSweeper) collector() { + defer s.wg.Done() for { // Clean inputs, which will remove inputs that are swept, @@ -737,25 +719,16 @@ func (s *UtxoSweeper) collector(blockEpochs <-chan *chainntnfs.BlockEpoch) { // A new block comes in, update the bestHeight, perform a check // over all pending inputs and publish sweeping txns if needed. - case epoch, ok := <-blockEpochs: - if !ok { - // We should stop the sweeper before stopping - // the chain service. Otherwise it indicates an - // error. - log.Error("Block epoch channel closed") - - return - } - + case beat := <-s.BlockbeatChan: // Update the sweeper to the best height. - s.currentHeight = epoch.Height + s.currentHeight = beat.Height() // Update the inputs with the latest height. inputs := s.updateSweeperInputs() log.Debugf("Received new block: height=%v, attempt "+ "sweeping %d inputs:\n%s", - epoch.Height, len(inputs), + s.currentHeight, len(inputs), lnutils.NewLogClosure(func() string { inps := make( []input.Input, 0, len(inputs), @@ -770,6 +743,9 @@ func (s *UtxoSweeper) collector(blockEpochs <-chan *chainntnfs.BlockEpoch) { // Attempt to sweep any pending inputs. s.sweepPendingInputs(inputs) + // Notify we've processed the block. + s.NotifyBlockProcessed(beat, nil) + case <-s.quit: return } From 3ac6752a778307fc0223fca29fdf61a38319fb3c Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Mon, 18 Nov 2024 11:09:21 +0800 Subject: [PATCH 20/92] sweep: remove redundant notifications during shutdown This commit removes the hack introduced in #4851. Previously we had this issue because the chain notifier was stopped before the sweeper, which was changed a while back and we now always stop the chain notifier last. In addition, since we no longer subscribe to the block epoch chan directly, this issue can no longer happen. --- sweep/sweeper.go | 33 +-------------------------------- 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/sweep/sweeper.go b/sweep/sweeper.go index 64306935c9b..b91a6808fbf 100644 --- a/sweep/sweeper.go +++ b/sweep/sweeper.go @@ -454,38 +454,7 @@ func (s *UtxoSweeper) Start() error { // Start sweeper main loop. s.wg.Add(1) - go func() { - defer s.wg.Done() - - s.collector() - - // The collector exited and won't longer handle incoming - // requests. This can happen on shutdown, when the block - // notifier shuts down before the sweeper and its clients. In - // order to not deadlock the clients waiting for their requests - // being handled, we handle them here and immediately return an - // error. When the sweeper finally is shut down we can exit as - // the clients will be notified. - for { - select { - case inp := <-s.newInputs: - inp.resultChan <- Result{ - Err: ErrSweeperShuttingDown, - } - - case req := <-s.pendingSweepsReqs: - req.errChan <- ErrSweeperShuttingDown - - case req := <-s.updateReqs: - req.responseChan <- &updateResp{ - err: ErrSweeperShuttingDown, - } - - case <-s.quit: - return - } - } - }() + go s.collector() return nil } From 5f9d473702fc42f2a8412bad1afe9f905011a40f Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 4 Jun 2024 20:53:33 +0800 Subject: [PATCH 21/92] contractcourt: remove `waitForHeight` in resolvers The sweeper can handle the waiting so there's no need to wait for blocks inside the resolvers. By offering the inputs prior to their mature heights also guarantees the inputs with the same deadline are aggregated. --- contractcourt/commit_sweep_resolver.go | 63 --------------------- contractcourt/commit_sweep_resolver_test.go | 26 ++------- contractcourt/htlc_success_resolver.go | 26 +-------- contractcourt/htlc_success_resolver_test.go | 4 -- contractcourt/htlc_timeout_resolver.go | 26 +-------- contractcourt/htlc_timeout_resolver_test.go | 5 -- 6 files changed, 6 insertions(+), 144 deletions(-) diff --git a/contractcourt/commit_sweep_resolver.go b/contractcourt/commit_sweep_resolver.go index 6019a0dbc6d..8e89d33fdca 100644 --- a/contractcourt/commit_sweep_resolver.go +++ b/contractcourt/commit_sweep_resolver.go @@ -101,36 +101,6 @@ func (c *commitSweepResolver) ResolverKey() []byte { return key[:] } -// waitForHeight registers for block notifications and waits for the provided -// block height to be reached. -func waitForHeight(waitHeight uint32, notifier chainntnfs.ChainNotifier, - quit <-chan struct{}) error { - - // Register for block epochs. After registration, the current height - // will be sent on the channel immediately. - blockEpochs, err := notifier.RegisterBlockEpochNtfn(nil) - if err != nil { - return err - } - defer blockEpochs.Cancel() - - for { - select { - case newBlock, ok := <-blockEpochs.Epochs: - if !ok { - return errResolverShuttingDown - } - height := newBlock.Height - if height >= int32(waitHeight) { - return nil - } - - case <-quit: - return errResolverShuttingDown - } - } -} - // waitForSpend waits for the given outpoint to be spent, and returns the // details of the spending tx. func waitForSpend(op *wire.OutPoint, pkScript []byte, heightHint uint32, @@ -225,39 +195,6 @@ func (c *commitSweepResolver) Resolve(_ bool) (ContractResolver, error) { c.currentReport.MaturityHeight = unlockHeight c.reportLock.Unlock() - // If there is a csv/cltv lock, we'll wait for that. - if c.commitResolution.MaturityDelay > 0 || c.hasCLTV() { - // Determine what height we should wait until for the locks to - // expire. - var waitHeight uint32 - switch { - // If we have both a csv and cltv lock, we'll need to look at - // both and see which expires later. - case c.commitResolution.MaturityDelay > 0 && c.hasCLTV(): - c.log.Debugf("waiting for CSV and CLTV lock to expire "+ - "at height %v", unlockHeight) - // If the CSV expires after the CLTV, or there is no - // CLTV, then we can broadcast a sweep a block before. - // Otherwise, we need to broadcast at our expected - // unlock height. - waitHeight = uint32(math.Max( - float64(unlockHeight-1), float64(c.leaseExpiry), - )) - - // If we only have a csv lock, wait for the height before the - // lock expires as the spend path should be unlocked by then. - case c.commitResolution.MaturityDelay > 0: - c.log.Debugf("waiting for CSV lock to expire at "+ - "height %v", unlockHeight) - waitHeight = unlockHeight - 1 - } - - err := waitForHeight(waitHeight, c.Notifier, c.quit) - if err != nil { - return nil, err - } - } - var ( isLocalCommitTx bool diff --git a/contractcourt/commit_sweep_resolver_test.go b/contractcourt/commit_sweep_resolver_test.go index 077fb8f82c2..85f5ba4e48f 100644 --- a/contractcourt/commit_sweep_resolver_test.go +++ b/contractcourt/commit_sweep_resolver_test.go @@ -90,12 +90,6 @@ func (i *commitSweepResolverTestContext) resolve() { }() } -func (i *commitSweepResolverTestContext) notifyEpoch(height int32) { - i.notifier.EpochChan <- &chainntnfs.BlockEpoch{ - Height: height, - } -} - func (i *commitSweepResolverTestContext) waitForResult() { i.t.Helper() @@ -292,22 +286,10 @@ func testCommitSweepResolverDelay(t *testing.T, sweepErr error) { t.Fatal("report maturity height incorrect") } - // Notify initial block height. The csv lock is still in effect, so we - // don't expect any sweep to happen yet. - ctx.notifyEpoch(testInitialBlockHeight) - - select { - case <-ctx.sweeper.sweptInputs: - t.Fatal("no sweep expected") - case <-time.After(sweepProcessInterval): - } - - // A new block arrives. The commit tx confirmed at height -1 and the csv - // is 3, so a spend will be valid in the first block after height +1. - ctx.notifyEpoch(testInitialBlockHeight + 1) - - <-ctx.sweeper.sweptInputs - + // Notify initial block height. Although the csv lock is still in + // effect, we expect the input being sent to the sweeper before the csv + // lock expires. + // // Set the resolution report outcome based on whether our sweep // succeeded. outcome := channeldb.ResolverOutcomeClaimed diff --git a/contractcourt/htlc_success_resolver.go b/contractcourt/htlc_success_resolver.go index 9d09f844dcb..9e655cc48ad 100644 --- a/contractcourt/htlc_success_resolver.go +++ b/contractcourt/htlc_success_resolver.go @@ -359,30 +359,6 @@ func (h *htlcSuccessResolver) broadcastReSignedSuccessTx(immediate bool) ( "height %v", h, h.htlc.RHash[:], waitHeight) } - // Deduct one block so this input is offered to the sweeper one block - // earlier since the sweeper will wait for one block to trigger the - // sweeping. - // - // TODO(yy): this is done so the outputs can be aggregated - // properly. Suppose CSV locks of five 2nd-level outputs all - // expire at height 840000, there is a race in block digestion - // between contractcourt and sweeper: - // - G1: block 840000 received in contractcourt, it now offers - // the outputs to the sweeper. - // - G2: block 840000 received in sweeper, it now starts to - // sweep the received outputs - there's no guarantee all - // fives have been received. - // To solve this, we either offer the outputs earlier, or - // implement `blockbeat`, and force contractcourt and sweeper - // to consume each block sequentially. - waitHeight-- - - // TODO(yy): let sweeper handles the wait? - err := waitForHeight(waitHeight, h.Notifier, h.quit) - if err != nil { - return nil, 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 @@ -421,7 +397,7 @@ func (h *htlcSuccessResolver) broadcastReSignedSuccessTx(immediate bool) ( h.htlc.RHash[:], budget, waitHeight) // TODO(roasbeef): need to update above for leased types - _, err = h.Sweeper.SweepInput( + _, err := h.Sweeper.SweepInput( inp, sweep.Params{ Budget: budget, diff --git a/contractcourt/htlc_success_resolver_test.go b/contractcourt/htlc_success_resolver_test.go index c0206d8f142..ae7f1b390aa 100644 --- a/contractcourt/htlc_success_resolver_test.go +++ b/contractcourt/htlc_success_resolver_test.go @@ -437,10 +437,6 @@ func TestHtlcSuccessSecondStageResolutionSweeper(t *testing.T) { } } - ctx.notifier.EpochChan <- &chainntnfs.BlockEpoch{ - Height: 13, - } - // We expect it to sweep the second-level // transaction we notfied about above. resolver := ctx.resolver.(*htlcSuccessResolver) diff --git a/contractcourt/htlc_timeout_resolver.go b/contractcourt/htlc_timeout_resolver.go index 545e7c61353..0f019a36a19 100644 --- a/contractcourt/htlc_timeout_resolver.go +++ b/contractcourt/htlc_timeout_resolver.go @@ -789,30 +789,6 @@ func (h *htlcTimeoutResolver) handleCommitSpend( "height %v", h, h.htlc.RHash[:], waitHeight) } - // Deduct one block so this input is offered to the sweeper one - // block earlier since the sweeper will wait for one block to - // trigger the sweeping. - // - // TODO(yy): this is done so the outputs can be aggregated - // properly. Suppose CSV locks of five 2nd-level outputs all - // expire at height 840000, there is a race in block digestion - // between contractcourt and sweeper: - // - G1: block 840000 received in contractcourt, it now offers - // the outputs to the sweeper. - // - G2: block 840000 received in sweeper, it now starts to - // sweep the received outputs - there's no guarantee all - // fives have been received. - // To solve this, we either offer the outputs earlier, or - // implement `blockbeat`, and force contractcourt and sweeper - // to consume each block sequentially. - waitHeight-- - - // TODO(yy): let sweeper handles the wait? - err := waitForHeight(waitHeight, h.Notifier, h.quit) - if err != nil { - return nil, 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 @@ -853,7 +829,7 @@ func (h *htlcTimeoutResolver) handleCommitSpend( "sweeper with no deadline and budget=%v at height=%v", h, h.htlc.RHash[:], budget, waitHeight) - _, err = h.Sweeper.SweepInput( + _, err := h.Sweeper.SweepInput( inp, sweep.Params{ Budget: budget, diff --git a/contractcourt/htlc_timeout_resolver_test.go b/contractcourt/htlc_timeout_resolver_test.go index 0e4f1336c27..0b6ccd5b399 100644 --- a/contractcourt/htlc_timeout_resolver_test.go +++ b/contractcourt/htlc_timeout_resolver_test.go @@ -1120,11 +1120,6 @@ func TestHtlcTimeoutSecondStageSweeper(t *testing.T) { t.Fatalf("resolution not sent") } - // Mimic CSV lock expiring. - ctx.notifier.EpochChan <- &chainntnfs.BlockEpoch{ - Height: 13, - } - // The timeout tx output should now be given to // the sweeper. resolver := ctx.resolver.(*htlcTimeoutResolver) From 045f8432b71937d4781dcd3f81d59ad939c147b3 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 29 Oct 2024 21:48:13 +0800 Subject: [PATCH 22/92] contractcourt: remove block subscription in chain arbitrator This commit removes the block subscriptions used in `ChainArbitrator` and replaced them with the blockbeat managed by `BlockbeatDispatcher`. --- contractcourt/breach_arbitrator_test.go | 2 +- contractcourt/chain_arbitrator.go | 124 +++++++----------------- contractcourt/chain_arbitrator_test.go | 2 - 3 files changed, 38 insertions(+), 90 deletions(-) diff --git a/contractcourt/breach_arbitrator_test.go b/contractcourt/breach_arbitrator_test.go index c387c217970..99ed852696c 100644 --- a/contractcourt/breach_arbitrator_test.go +++ b/contractcourt/breach_arbitrator_test.go @@ -36,7 +36,7 @@ import ( ) var ( - defaultTimeout = 30 * time.Second + defaultTimeout = 10 * time.Second breachOutPoints = []wire.OutPoint{ { diff --git a/contractcourt/chain_arbitrator.go b/contractcourt/chain_arbitrator.go index e8ba2907aa5..0a42dd57b76 100644 --- a/contractcourt/chain_arbitrator.go +++ b/contractcourt/chain_arbitrator.go @@ -267,6 +267,9 @@ type ChainArbitrator struct { // active channels that it must still watch over. chanSource *channeldb.DB + // beat is the current best known blockbeat. + beat chainio.Blockbeat + quit chan struct{} wg sync.WaitGroup @@ -797,18 +800,11 @@ func (c *ChainArbitrator) Start() error { } } - // Subscribe to a single stream of block epoch notifications that we - // will dispatch to all active arbitrators. - blockEpoch, err := c.cfg.Notifier.RegisterBlockEpochNtfn(nil) - if err != nil { - return err - } - // Start our goroutine which will dispatch blocks to each arbitrator. c.wg.Add(1) go func() { defer c.wg.Done() - c.dispatchBlocks(blockEpoch) + c.dispatchBlocks() }() // TODO(roasbeef): eventually move all breach watching here @@ -816,94 +812,22 @@ func (c *ChainArbitrator) Start() error { return nil } -// blockRecipient contains the information we need to dispatch a block to a -// channel arbitrator. -type blockRecipient struct { - // chanPoint is the funding outpoint of the channel. - chanPoint wire.OutPoint - - // blocks is the channel that new block heights are sent into. This - // channel should be sufficiently buffered as to not block the sender. - blocks chan<- int32 - - // quit is closed if the receiving entity is shutting down. - quit chan struct{} -} - // dispatchBlocks consumes a block epoch notification stream and dispatches // blocks to each of the chain arb's active channel arbitrators. This function // must be run in a goroutine. -func (c *ChainArbitrator) dispatchBlocks( - blockEpoch *chainntnfs.BlockEpochEvent) { - - // getRecipients is a helper function which acquires the chain arb - // lock and returns a set of block recipients which can be used to - // dispatch blocks. - getRecipients := func() []blockRecipient { - c.Lock() - blocks := make([]blockRecipient, 0, len(c.activeChannels)) - for _, channel := range c.activeChannels { - blocks = append(blocks, blockRecipient{ - chanPoint: channel.cfg.ChanPoint, - blocks: channel.blocks, - quit: channel.quit, - }) - } - c.Unlock() - - return blocks - } - - // On exit, cancel our blocks subscription and close each block channel - // so that the arbitrators know they will no longer be receiving blocks. - defer func() { - blockEpoch.Cancel() - - recipients := getRecipients() - for _, recipient := range recipients { - close(recipient.blocks) - } - }() - +func (c *ChainArbitrator) dispatchBlocks() { // Consume block epochs until we receive the instruction to shutdown. for { select { // Consume block epochs, exiting if our subscription is // terminated. - case block, ok := <-blockEpoch.Epochs: - if !ok { - log.Trace("dispatchBlocks block epoch " + - "cancelled") - return - } + case beat := <-c.BlockbeatChan: + // Set the current blockbeat. + c.beat = beat - // Get the set of currently active channels block - // subscription channels and dispatch the block to - // each. - for _, recipient := range getRecipients() { - select { - // Deliver the block to the arbitrator. - case recipient.blocks <- block.Height: - - // If the recipient is shutting down, exit - // without delivering the block. This may be - // the case when two blocks are mined in quick - // succession, and the arbitrator resolves - // after the first block, and does not need to - // consume the second block. - case <-recipient.quit: - log.Debugf("channel: %v exit without "+ - "receiving block: %v", - recipient.chanPoint, - block.Height) - - // If the chain arb is shutting down, we don't - // need to deliver any more blocks (everything - // will be shutting down). - case <-c.quit: - return - } - } + // Send this blockbeat to all the active channels and + // wait for them to finish processing it. + c.handleBlockbeat(beat) // Exit if the chain arbitrator is shutting down. case <-c.quit: @@ -912,6 +836,32 @@ func (c *ChainArbitrator) dispatchBlocks( } } +// handleBlockbeat sends the blockbeat to all active channel arbitrator in +// parallel and wait for them to finish processing it. +func (c *ChainArbitrator) handleBlockbeat(beat chainio.Blockbeat) { + // Read the active channels in a lock. + c.Lock() + + // Create a slice to record active channel arbitrator. + channels := make([]chainio.Consumer, 0, len(c.activeChannels)) + + // Copy the active channels to the slice. + for _, channel := range c.activeChannels { + channels = append(channels, channel) + } + + c.Unlock() + + // Iterate all the copied channels and send the blockbeat to them. + // + // NOTE: This method will timeout if the processing of blocks of the + // subsystems is too long (60s). + err := chainio.DispatchConcurrent(beat, channels) + + // Notify the chain arbitrator has processed the block. + c.NotifyBlockProcessed(beat, err) +} + // republishClosingTxs will load any stored cooperative or unilateral closing // transactions and republish them. This helps ensure propagation of the // transactions in the event that prior publications failed. diff --git a/contractcourt/chain_arbitrator_test.go b/contractcourt/chain_arbitrator_test.go index fe2603ca5a6..de6d69900b4 100644 --- a/contractcourt/chain_arbitrator_test.go +++ b/contractcourt/chain_arbitrator_test.go @@ -77,7 +77,6 @@ func TestChainArbitratorRepublishCloses(t *testing.T) { ChainIO: &mock.ChainIO{}, Notifier: &mock.ChainNotifier{ SpendChan: make(chan *chainntnfs.SpendDetail), - EpochChan: make(chan *chainntnfs.BlockEpoch), ConfChan: make(chan *chainntnfs.TxConfirmation), }, PublishTx: func(tx *wire.MsgTx, _ string) error { @@ -158,7 +157,6 @@ func TestResolveContract(t *testing.T) { ChainIO: &mock.ChainIO{}, Notifier: &mock.ChainNotifier{ SpendChan: make(chan *chainntnfs.SpendDetail), - EpochChan: make(chan *chainntnfs.BlockEpoch), ConfChan: make(chan *chainntnfs.TxConfirmation), }, PublishTx: func(tx *wire.MsgTx, _ string) error { From 71295534bb848205d51354e21b6b625cf55fb61d Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 29 Oct 2024 21:48:58 +0800 Subject: [PATCH 23/92] contractcourt: remove block subscription in channel arbitrator This commit removes the block subscriptions used in `ChannelArbitrator`, replaced them with the blockbeat managed by `BlockbeatDispatcher`. --- contractcourt/channel_arbitrator.go | 53 +++++++++++++----------- contractcourt/channel_arbitrator_test.go | 48 +++++++++++++++++---- 2 files changed, 68 insertions(+), 33 deletions(-) diff --git a/contractcourt/channel_arbitrator.go b/contractcourt/channel_arbitrator.go index 0c8dde532ec..666ad867862 100644 --- a/contractcourt/channel_arbitrator.go +++ b/contractcourt/channel_arbitrator.go @@ -357,11 +357,6 @@ type ChannelArbitrator struct { // to do its duty. cfg ChannelArbitratorConfig - // blocks is a channel that the arbitrator will receive new blocks on. - // This channel should be buffered by so that it does not block the - // sender. - blocks chan int32 - // signalUpdates is a channel that any new live signals for the channel // we're watching over will be sent. signalUpdates chan *signalUpdateMsg @@ -411,7 +406,6 @@ func NewChannelArbitrator(cfg ChannelArbitratorConfig, c := &ChannelArbitrator{ log: log, - blocks: make(chan int32, arbitratorBlockBufferSize), signalUpdates: make(chan *signalUpdateMsg), resolutionSignal: make(chan struct{}), forceCloseReqs: make(chan *forceCloseReq), @@ -2769,31 +2763,21 @@ func (c *ChannelArbitrator) channelAttendant(bestHeight int32, // A new block has arrived, we'll examine all the active HTLC's // to see if any of them have expired, and also update our // track of the best current height. - case blockHeight, ok := <-c.blocks: - if !ok { - return - } - bestHeight = blockHeight + case beat := <-c.BlockbeatChan: + bestHeight = beat.Height() - // If we're not in the default state, then we can - // ignore this signal as we're waiting for contract - // resolution. - if c.state != StateDefault { - continue - } + log.Debugf("ChannelArbitrator(%v): new block height=%v", + c.cfg.ChanPoint, bestHeight) - // Now that a new block has arrived, we'll attempt to - // advance our state forward. - nextState, _, err := c.advanceState( - uint32(bestHeight), chainTrigger, nil, - ) + err := c.handleBlockbeat(beat) if err != nil { - log.Errorf("Unable to advance state: %v", err) + log.Errorf("Handle block=%v got err: %v", + bestHeight, err) } // If as a result of this trigger, the contract is // fully resolved, then well exit. - if nextState == StateFullyResolved { + if c.state == StateFullyResolved { return } @@ -3144,6 +3128,27 @@ func (c *ChannelArbitrator) channelAttendant(bestHeight int32, } } +// handleBlockbeat processes a newly received blockbeat by advancing the +// arbitrator's internal state using the received block height. +func (c *ChannelArbitrator) handleBlockbeat(beat chainio.Blockbeat) error { + // Notify we've processed the block. + defer c.NotifyBlockProcessed(beat, nil) + + // Try to advance the state if we are in StateDefault. + if c.state == StateDefault { + // Now that a new block has arrived, we'll attempt to advance + // our state forward. + _, _, err := c.advanceState( + uint32(beat.Height()), chainTrigger, nil, + ) + if err != nil { + return fmt.Errorf("unable to advance state: %w", err) + } + } + + return nil +} + // Name returns a human-readable string for this subsystem. // // NOTE: Part of chainio.Consumer interface. diff --git a/contractcourt/channel_arbitrator_test.go b/contractcourt/channel_arbitrator_test.go index 7150e5aea4d..8715e1e63de 100644 --- a/contractcourt/channel_arbitrator_test.go +++ b/contractcourt/channel_arbitrator_test.go @@ -13,6 +13,7 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/chainio" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/clock" @@ -227,6 +228,15 @@ func (c *chanArbTestCtx) CleanUp() { } } +// receiveBlockbeat mocks the behavior of a blockbeat being sent by the +// BlockbeatDispatcher, which essentially mocks the method `ProcessBlock`. +func (c *chanArbTestCtx) receiveBlockbeat(height int) { + go func() { + beat := newBeatFromHeight(int32(height)) + c.chanArb.BlockbeatChan <- beat + }() +} + // AssertStateTransitions asserts that the state machine steps through the // passed states in order. func (c *chanArbTestCtx) AssertStateTransitions(expectedStates ...ArbitratorState) { @@ -1037,7 +1047,7 @@ func TestChannelArbitratorLocalForceClosePendingHtlc(t *testing.T) { } require.Equal(t, expectedFinalHtlcs, chanArbCtx.finalHtlcs) - // We'll no re-create the resolver, notice that we use the existing + // We'll now re-create the resolver, notice that we use the existing // arbLog so it carries over the same on-disk state. chanArbCtxNew, err := chanArbCtx.Restart(nil) require.NoError(t, err, "unable to create ChannelArbitrator") @@ -1084,7 +1094,12 @@ func TestChannelArbitratorLocalForceClosePendingHtlc(t *testing.T) { } // Send a notification that the expiry height has been reached. + // + // TODO(yy): remove the EpochChan and use the blockbeat below once + // resolvers are hooked with the blockbeat. oldNotifier.EpochChan <- &chainntnfs.BlockEpoch{Height: 10} + // beat := chainio.NewBlockbeatFromHeight(10) + // chanArb.BlockbeatChan <- beat // htlcOutgoingContestResolver is now transforming into a // htlcTimeoutResolver and should send the contract off for incubation. @@ -1924,7 +1939,8 @@ func TestChannelArbitratorDanglingCommitForceClose(t *testing.T) { // now mine a block (height 5), which is 5 blocks away // (our grace delta) from the expiry of that HTLC. case testCase.htlcExpired: - chanArbCtx.chanArb.blocks <- 5 + beat := newBeatFromHeight(5) + chanArbCtx.chanArb.BlockbeatChan <- beat // Otherwise, we'll just trigger a regular force close // request. @@ -2036,8 +2052,7 @@ func TestChannelArbitratorDanglingCommitForceClose(t *testing.T) { // so instead, we'll mine another block which'll cause // it to re-examine its state and realize there're no // more HTLCs. - chanArbCtx.chanArb.blocks <- 6 - chanArbCtx.AssertStateTransitions(StateFullyResolved) + chanArbCtx.receiveBlockbeat(6) }) } } @@ -2108,13 +2123,15 @@ func TestChannelArbitratorPendingExpiredHTLC(t *testing.T) { // We will advance the uptime to 10 seconds which should be still within // the grace period and should not trigger going to chain. testClock.SetTime(startTime.Add(time.Second * 10)) - chanArbCtx.chanArb.blocks <- 5 + beat := newBeatFromHeight(5) + chanArbCtx.chanArb.BlockbeatChan <- beat chanArbCtx.AssertState(StateDefault) // We will advance the uptime to 16 seconds which should trigger going // to chain. testClock.SetTime(startTime.Add(time.Second * 16)) - chanArbCtx.chanArb.blocks <- 6 + beat = newBeatFromHeight(6) + chanArbCtx.chanArb.BlockbeatChan <- beat chanArbCtx.AssertStateTransitions( StateBroadcastCommit, StateCommitmentBroadcasted, @@ -2482,7 +2499,7 @@ func TestSweepAnchors(t *testing.T) { // Set current block height. heightHint := uint32(1000) - chanArbCtx.chanArb.blocks <- int32(heightHint) + chanArbCtx.receiveBlockbeat(int(heightHint)) htlcIndexBase := uint64(99) deadlineDelta := uint32(10) @@ -2645,7 +2662,7 @@ func TestSweepLocalAnchor(t *testing.T) { // Set current block height. heightHint := uint32(1000) - chanArbCtx.chanArb.blocks <- int32(heightHint) + chanArbCtx.receiveBlockbeat(int(heightHint)) htlcIndex := uint64(99) deadlineDelta := uint32(10) @@ -2793,7 +2810,8 @@ func TestChannelArbitratorAnchors(t *testing.T) { // Set current block height. heightHint := uint32(1000) - chanArbCtx.chanArb.blocks <- int32(heightHint) + beat := newBeatFromHeight(int32(heightHint)) + chanArbCtx.chanArb.BlockbeatChan <- beat htlcAmt := lnwire.MilliSatoshi(1_000_000) @@ -2960,10 +2978,14 @@ func TestChannelArbitratorAnchors(t *testing.T) { // to htlcWithPreimage's CLTV. require.Equal(t, 2, len(chanArbCtx.sweeper.deadlines)) require.EqualValues(t, + heightHint+deadlinePreimageDelta/2, + chanArbCtx.sweeper.deadlines[0], "want %d, got %d", heightHint+deadlinePreimageDelta/2, chanArbCtx.sweeper.deadlines[0], ) require.EqualValues(t, + heightHint+deadlinePreimageDelta/2, + chanArbCtx.sweeper.deadlines[1], "want %d, got %d", heightHint+deadlinePreimageDelta/2, chanArbCtx.sweeper.deadlines[1], ) @@ -3146,3 +3168,11 @@ func (m *mockChannel) ForceCloseChan() (*wire.MsgTx, error) { return &wire.MsgTx{}, nil } + +func newBeatFromHeight(height int32) *chainio.Beat { + epoch := chainntnfs.BlockEpoch{ + Height: height, + } + + return chainio.NewBeat(epoch) +} From e2e59bd90c8b61f16ebe06fc70d5e744e7d98a8e Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Wed, 5 Jun 2024 00:56:39 +0800 Subject: [PATCH 24/92] contractcourt: remove the `immediate` param used in `Resolve` This `immediate` flag was added as a hack so during a restart, the pending resolvers would offer the inputs to the sweeper and ask it to sweep them immediately. This is no longer need due to `blockbeat`, as now during restart, a block is always sent to all subsystems via the flow `ChainArb` -> `ChannelArb` -> resolvers -> sweeper. Thus, when there are pending inputs offered, they will be processed by the sweeper immediately. --- contractcourt/anchor_resolver.go | 2 +- contractcourt/breach_resolver.go | 2 +- contractcourt/channel_arbitrator.go | 20 +++++++--------- contractcourt/commit_sweep_resolver.go | 4 +--- contractcourt/commit_sweep_resolver_test.go | 2 +- contractcourt/contract_resolver.go | 2 +- .../htlc_incoming_contest_resolver.go | 4 +--- .../htlc_incoming_contest_resolver_test.go | 2 +- .../htlc_outgoing_contest_resolver.go | 4 +--- .../htlc_outgoing_contest_resolver_test.go | 2 +- contractcourt/htlc_success_resolver.go | 24 +++++++------------ contractcourt/htlc_success_resolver_test.go | 2 +- contractcourt/htlc_timeout_resolver.go | 20 +++++++--------- contractcourt/htlc_timeout_resolver_test.go | 2 +- 14 files changed, 36 insertions(+), 56 deletions(-) diff --git a/contractcourt/anchor_resolver.go b/contractcourt/anchor_resolver.go index e482c4c7132..af7ac76462a 100644 --- a/contractcourt/anchor_resolver.go +++ b/contractcourt/anchor_resolver.go @@ -84,7 +84,7 @@ func (c *anchorResolver) ResolverKey() []byte { } // Resolve offers the anchor output to the sweeper and waits for it to be swept. -func (c *anchorResolver) Resolve(_ bool) (ContractResolver, error) { +func (c *anchorResolver) Resolve() (ContractResolver, error) { // Attempt to update the sweep parameters to the post-confirmation // situation. We don't want to force sweep anymore, because the anchor // lost its special purpose to get the commitment confirmed. It is just diff --git a/contractcourt/breach_resolver.go b/contractcourt/breach_resolver.go index 740b4471d5d..63395651cc3 100644 --- a/contractcourt/breach_resolver.go +++ b/contractcourt/breach_resolver.go @@ -47,7 +47,7 @@ func (b *breachResolver) ResolverKey() []byte { // been broadcast. // // TODO(yy): let sweeper handle the breach inputs. -func (b *breachResolver) Resolve(_ bool) (ContractResolver, error) { +func (b *breachResolver) Resolve() (ContractResolver, error) { if !b.subscribed { complete, err := b.SubscribeBreachComplete( &b.ChanPoint, b.replyChan, diff --git a/contractcourt/channel_arbitrator.go b/contractcourt/channel_arbitrator.go index 666ad867862..c9e8ff24329 100644 --- a/contractcourt/channel_arbitrator.go +++ b/contractcourt/channel_arbitrator.go @@ -816,7 +816,7 @@ func (c *ChannelArbitrator) relaunchResolvers(commitSet *CommitSet, // TODO(roasbeef): this isn't re-launched? } - c.launchResolvers(unresolvedContracts, true) + c.launchResolvers(unresolvedContracts) return nil } @@ -1355,7 +1355,7 @@ func (c *ChannelArbitrator) stateStep( // Finally, we'll launch all the required contract resolvers. // Once they're all resolved, we're no longer needed. - c.launchResolvers(resolvers, false) + c.launchResolvers(resolvers) nextState = StateWaitingFullResolution @@ -1579,16 +1579,14 @@ func (c *ChannelArbitrator) findCommitmentDeadlineAndValue(heightHint uint32, } // launchResolvers updates the activeResolvers list and starts the resolvers. -func (c *ChannelArbitrator) launchResolvers(resolvers []ContractResolver, - immediate bool) { - +func (c *ChannelArbitrator) launchResolvers(resolvers []ContractResolver) { c.activeResolversLock.Lock() - defer c.activeResolversLock.Unlock() - c.activeResolvers = resolvers + c.activeResolversLock.Unlock() + for _, contract := range resolvers { c.wg.Add(1) - go c.resolveContract(contract, immediate) + go c.resolveContract(contract) } } @@ -2560,9 +2558,7 @@ func (c *ChannelArbitrator) replaceResolver(oldResolver, // contracts. // // NOTE: This MUST be run as a goroutine. -func (c *ChannelArbitrator) resolveContract(currentContract ContractResolver, - immediate bool) { - +func (c *ChannelArbitrator) resolveContract(currentContract ContractResolver) { defer c.wg.Done() log.Debugf("ChannelArbitrator(%v): attempting to resolve %T", @@ -2583,7 +2579,7 @@ func (c *ChannelArbitrator) resolveContract(currentContract ContractResolver, default: // Otherwise, we'll attempt to resolve the current // contract. - nextContract, err := currentContract.Resolve(immediate) + nextContract, err := currentContract.Resolve() if err != nil { if err == errResolverShuttingDown { return diff --git a/contractcourt/commit_sweep_resolver.go b/contractcourt/commit_sweep_resolver.go index 8e89d33fdca..9a3bb058ae3 100644 --- a/contractcourt/commit_sweep_resolver.go +++ b/contractcourt/commit_sweep_resolver.go @@ -165,9 +165,7 @@ func (c *commitSweepResolver) getCommitTxConfHeight() (uint32, error) { // returned. // // NOTE: This function MUST be run as a goroutine. -// -//nolint:funlen -func (c *commitSweepResolver) Resolve(_ bool) (ContractResolver, error) { +func (c *commitSweepResolver) Resolve() (ContractResolver, error) { // If we're already resolved, then we can exit early. if c.resolved { return nil, nil diff --git a/contractcourt/commit_sweep_resolver_test.go b/contractcourt/commit_sweep_resolver_test.go index 85f5ba4e48f..2195e33779f 100644 --- a/contractcourt/commit_sweep_resolver_test.go +++ b/contractcourt/commit_sweep_resolver_test.go @@ -82,7 +82,7 @@ func (i *commitSweepResolverTestContext) resolve() { // Start resolver. i.resolverResultChan = make(chan resolveResult, 1) go func() { - nextResolver, err := i.resolver.Resolve(false) + nextResolver, err := i.resolver.Resolve() i.resolverResultChan <- resolveResult{ nextResolver: nextResolver, err: err, diff --git a/contractcourt/contract_resolver.go b/contractcourt/contract_resolver.go index f5a88f24e68..3629c1bc3c6 100644 --- a/contractcourt/contract_resolver.go +++ b/contractcourt/contract_resolver.go @@ -42,7 +42,7 @@ type ContractResolver interface { // resolution, then another resolve is returned. // // NOTE: This function MUST be run as a goroutine. - Resolve(immediate bool) (ContractResolver, error) + Resolve() (ContractResolver, error) // SupplementState allows the user of a ContractResolver to supplement // it with state required for the proper resolution of a contract. diff --git a/contractcourt/htlc_incoming_contest_resolver.go b/contractcourt/htlc_incoming_contest_resolver.go index ee526dfff3d..0714fb13018 100644 --- a/contractcourt/htlc_incoming_contest_resolver.go +++ b/contractcourt/htlc_incoming_contest_resolver.go @@ -90,9 +90,7 @@ func (h *htlcIncomingContestResolver) processFinalHtlcFail() error { // as we have no remaining actions left at our disposal. // // NOTE: Part of the ContractResolver interface. -func (h *htlcIncomingContestResolver) Resolve( - _ bool) (ContractResolver, error) { - +func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) { // If we're already full resolved, then we don't have anything further // to do. if h.resolved { diff --git a/contractcourt/htlc_incoming_contest_resolver_test.go b/contractcourt/htlc_incoming_contest_resolver_test.go index 22280f953e7..e8b8eac0c96 100644 --- a/contractcourt/htlc_incoming_contest_resolver_test.go +++ b/contractcourt/htlc_incoming_contest_resolver_test.go @@ -395,7 +395,7 @@ func (i *incomingResolverTestContext) resolve() { i.resolveErr = make(chan error, 1) go func() { var err error - i.nextResolver, err = i.resolver.Resolve(false) + i.nextResolver, err = i.resolver.Resolve() i.resolveErr <- err }() diff --git a/contractcourt/htlc_outgoing_contest_resolver.go b/contractcourt/htlc_outgoing_contest_resolver.go index 1303d0af600..ec32ff7f172 100644 --- a/contractcourt/htlc_outgoing_contest_resolver.go +++ b/contractcourt/htlc_outgoing_contest_resolver.go @@ -49,9 +49,7 @@ func newOutgoingContestResolver(res lnwallet.OutgoingHtlcResolution, // When either of these two things happens, we'll create a new resolver which // is able to handle the final resolution of the contract. We're only the pivot // point. -func (h *htlcOutgoingContestResolver) Resolve( - _ bool) (ContractResolver, error) { - +func (h *htlcOutgoingContestResolver) Resolve() (ContractResolver, error) { // If we're already full resolved, then we don't have anything further // to do. if h.resolved { diff --git a/contractcourt/htlc_outgoing_contest_resolver_test.go b/contractcourt/htlc_outgoing_contest_resolver_test.go index 6608a6fb519..4fa3a6874f9 100644 --- a/contractcourt/htlc_outgoing_contest_resolver_test.go +++ b/contractcourt/htlc_outgoing_contest_resolver_test.go @@ -209,7 +209,7 @@ func (i *outgoingResolverTestContext) resolve() { // Start resolver. i.resolverResultChan = make(chan resolveResult, 1) go func() { - nextResolver, err := i.resolver.Resolve(false) + nextResolver, err := i.resolver.Resolve() i.resolverResultChan <- resolveResult{ nextResolver: nextResolver, err: err, diff --git a/contractcourt/htlc_success_resolver.go b/contractcourt/htlc_success_resolver.go index 9e655cc48ad..06ebf4edc4a 100644 --- a/contractcourt/htlc_success_resolver.go +++ b/contractcourt/htlc_success_resolver.go @@ -115,9 +115,7 @@ func (h *htlcSuccessResolver) ResolverKey() []byte { // TODO(roasbeef): create multi to batch // // NOTE: Part of the ContractResolver interface. -func (h *htlcSuccessResolver) Resolve( - immediate bool) (ContractResolver, error) { - +func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) { // If we're already resolved, then we can exit early. if h.resolved { return nil, nil @@ -126,12 +124,12 @@ func (h *htlcSuccessResolver) Resolve( // If we don't have a success transaction, then this means that this is // an output on the remote party's commitment transaction. if h.htlcResolution.SignedSuccessTx == nil { - return h.resolveRemoteCommitOutput(immediate) + return h.resolveRemoteCommitOutput() } // Otherwise this an output on our own commitment, and we must start by // broadcasting the second-level success transaction. - secondLevelOutpoint, err := h.broadcastSuccessTx(immediate) + secondLevelOutpoint, err := h.broadcastSuccessTx() if err != nil { return nil, err } @@ -165,8 +163,8 @@ func (h *htlcSuccessResolver) Resolve( // broadcasting the second-level success transaction. It returns the ultimate // outpoint of the second-level tx, that we must wait to be spent for the // resolver to be fully resolved. -func (h *htlcSuccessResolver) broadcastSuccessTx( - immediate bool) (*wire.OutPoint, error) { +func (h *htlcSuccessResolver) broadcastSuccessTx() ( + *wire.OutPoint, error) { // If we have non-nil SignDetails, this means that have a 2nd level // HTLC transaction that is signed using sighash SINGLE|ANYONECANPAY @@ -175,7 +173,7 @@ func (h *htlcSuccessResolver) broadcastSuccessTx( // the checkpointed outputIncubating field to determine if we already // swept the HTLC output into the second level transaction. if h.htlcResolution.SignDetails != nil { - return h.broadcastReSignedSuccessTx(immediate) + return h.broadcastReSignedSuccessTx() } // Otherwise we'll publish the second-level transaction directly and @@ -225,10 +223,8 @@ func (h *htlcSuccessResolver) broadcastSuccessTx( // broadcastReSignedSuccessTx handles the case where we have non-nil // SignDetails, and offers the second level transaction to the Sweeper, that // will re-sign it and attach fees at will. -// -//nolint:funlen -func (h *htlcSuccessResolver) broadcastReSignedSuccessTx(immediate bool) ( - *wire.OutPoint, error) { +func (h *htlcSuccessResolver) broadcastReSignedSuccessTx() (*wire.OutPoint, + error) { // Keep track of the tx spending the HTLC output on the commitment, as // this will be the confirmed second-level tx we'll ultimately sweep. @@ -287,7 +283,6 @@ func (h *htlcSuccessResolver) broadcastReSignedSuccessTx(immediate bool) ( sweep.Params{ Budget: budget, DeadlineHeight: deadline, - Immediate: immediate, }, ) if err != nil { @@ -419,7 +414,7 @@ func (h *htlcSuccessResolver) broadcastReSignedSuccessTx(immediate bool) ( // resolveRemoteCommitOutput handles sweeping an HTLC output on the remote // commitment with the preimage. In this case we can sweep the output directly, // and don't have to broadcast a second-level transaction. -func (h *htlcSuccessResolver) resolveRemoteCommitOutput(immediate bool) ( +func (h *htlcSuccessResolver) resolveRemoteCommitOutput() ( ContractResolver, error) { isTaproot := txscript.IsPayToTaproot( @@ -471,7 +466,6 @@ func (h *htlcSuccessResolver) resolveRemoteCommitOutput(immediate bool) ( sweep.Params{ Budget: budget, DeadlineHeight: deadline, - Immediate: immediate, }, ) if err != nil { diff --git a/contractcourt/htlc_success_resolver_test.go b/contractcourt/htlc_success_resolver_test.go index ae7f1b390aa..75c733638fe 100644 --- a/contractcourt/htlc_success_resolver_test.go +++ b/contractcourt/htlc_success_resolver_test.go @@ -134,7 +134,7 @@ func (i *htlcResolverTestContext) resolve() { // Start resolver. i.resolverResultChan = make(chan resolveResult, 1) go func() { - nextResolver, err := i.resolver.Resolve(false) + nextResolver, err := i.resolver.Resolve() i.resolverResultChan <- resolveResult{ nextResolver: nextResolver, err: err, diff --git a/contractcourt/htlc_timeout_resolver.go b/contractcourt/htlc_timeout_resolver.go index 0f019a36a19..81d8e85d21c 100644 --- a/contractcourt/htlc_timeout_resolver.go +++ b/contractcourt/htlc_timeout_resolver.go @@ -418,9 +418,7 @@ func checkSizeAndIndex(witness wire.TxWitness, size, index int) bool { // see a direct sweep via the timeout clause. // // NOTE: Part of the ContractResolver interface. -func (h *htlcTimeoutResolver) Resolve( - immediate bool) (ContractResolver, error) { - +func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) { // If we're already resolved, then we can exit early. if h.resolved { return nil, nil @@ -429,7 +427,7 @@ func (h *htlcTimeoutResolver) Resolve( // Start by spending the HTLC output, either by broadcasting the // second-level timeout transaction, or directly if this is the remote // commitment. - commitSpend, err := h.spendHtlcOutput(immediate) + commitSpend, err := h.spendHtlcOutput() if err != nil { return nil, err } @@ -477,7 +475,7 @@ func (h *htlcTimeoutResolver) Resolve( // sweepSecondLevelTx sends a second level timeout transaction to the sweeper. // This transaction uses the SINLGE|ANYONECANPAY flag. -func (h *htlcTimeoutResolver) sweepSecondLevelTx(immediate bool) error { +func (h *htlcTimeoutResolver) sweepSecondLevelTx() error { log.Infof("%T(%x): offering second-layer timeout tx to sweeper: %v", h, h.htlc.RHash[:], spew.Sdump(h.htlcResolution.SignedTimeoutTx)) @@ -538,7 +536,6 @@ func (h *htlcTimeoutResolver) sweepSecondLevelTx(immediate bool) error { sweep.Params{ Budget: budget, DeadlineHeight: h.incomingHTLCExpiryHeight, - Immediate: immediate, }, ) if err != nil { @@ -572,7 +569,7 @@ func (h *htlcTimeoutResolver) sendSecondLevelTxLegacy() error { // sweeper. This is used when the remote party goes on chain, and we're able to // sweep an HTLC we offered after a timeout. Only the CLTV encumbered outputs // are resolved via this path. -func (h *htlcTimeoutResolver) sweepDirectHtlcOutput(immediate bool) error { +func (h *htlcTimeoutResolver) sweepDirectHtlcOutput() error { var htlcWitnessType input.StandardWitnessType if h.isTaproot() { htlcWitnessType = input.TaprootHtlcOfferedRemoteTimeout @@ -612,7 +609,6 @@ func (h *htlcTimeoutResolver) sweepDirectHtlcOutput(immediate bool) error { // This is an outgoing HTLC, so we want to make sure // that we sweep it before the incoming HTLC expires. DeadlineHeight: h.incomingHTLCExpiryHeight, - Immediate: immediate, }, ) if err != nil { @@ -627,8 +623,8 @@ func (h *htlcTimeoutResolver) sweepDirectHtlcOutput(immediate bool) error { // used to spend the output into the next stage. If this is the remote // commitment, the output will be swept directly without the timeout // transaction. -func (h *htlcTimeoutResolver) spendHtlcOutput( - immediate bool) (*chainntnfs.SpendDetail, error) { +func (h *htlcTimeoutResolver) spendHtlcOutput() ( + *chainntnfs.SpendDetail, error) { switch { // If we have non-nil SignDetails, this means that have a 2nd level @@ -636,7 +632,7 @@ func (h *htlcTimeoutResolver) spendHtlcOutput( // (the case for anchor type channels). In this case we can re-sign it // and attach fees at will. We let the sweeper handle this job. case h.htlcResolution.SignDetails != nil && !h.outputIncubating: - if err := h.sweepSecondLevelTx(immediate); err != nil { + if err := h.sweepSecondLevelTx(); err != nil { log.Errorf("Sending timeout tx to sweeper: %v", err) return nil, err @@ -645,7 +641,7 @@ func (h *htlcTimeoutResolver) spendHtlcOutput( // If this is a remote commitment there's no second level timeout txn, // and we can just send this directly to the sweeper. case h.htlcResolution.SignedTimeoutTx == nil && !h.outputIncubating: - if err := h.sweepDirectHtlcOutput(immediate); err != nil { + if err := h.sweepDirectHtlcOutput(); err != nil { log.Errorf("Sending direct spend to sweeper: %v", err) return nil, err diff --git a/contractcourt/htlc_timeout_resolver_test.go b/contractcourt/htlc_timeout_resolver_test.go index 0b6ccd5b399..17341f0c20b 100644 --- a/contractcourt/htlc_timeout_resolver_test.go +++ b/contractcourt/htlc_timeout_resolver_test.go @@ -390,7 +390,7 @@ func testHtlcTimeoutResolver(t *testing.T, testCase htlcTimeoutTestCase) { go func() { defer wg.Done() - _, err := resolver.Resolve(false) + _, err := resolver.Resolve() if err != nil { resolveErr <- err } From 802353036edd13715661304e70b8053e57a39708 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 29 Oct 2024 22:01:16 +0800 Subject: [PATCH 25/92] contractcourt: start channel arbitrator with blockbeat To avoid calling GetBestBlock again. --- contractcourt/chain_arbitrator.go | 4 +- contractcourt/channel_arbitrator.go | 10 ++-- contractcourt/channel_arbitrator_test.go | 65 +++++++++++++++--------- 3 files changed, 48 insertions(+), 31 deletions(-) diff --git a/contractcourt/chain_arbitrator.go b/contractcourt/chain_arbitrator.go index 0a42dd57b76..34ed7d7ff55 100644 --- a/contractcourt/chain_arbitrator.go +++ b/contractcourt/chain_arbitrator.go @@ -794,7 +794,7 @@ func (c *ChainArbitrator) Start() error { arbitrator.cfg.ChanPoint) } - if err := arbitrator.Start(startState); err != nil { + if err := arbitrator.Start(startState, c.beat); err != nil { stopAndLog() return err } @@ -1211,7 +1211,7 @@ func (c *ChainArbitrator) WatchNewChannel(newChan *channeldb.OpenChannel) error // arbitrators, then launch it. c.activeChannels[chanPoint] = channelArb - if err := channelArb.Start(nil); err != nil { + if err := channelArb.Start(nil, c.beat); err != nil { return err } diff --git a/contractcourt/channel_arbitrator.go b/contractcourt/channel_arbitrator.go index c9e8ff24329..85f082111a8 100644 --- a/contractcourt/channel_arbitrator.go +++ b/contractcourt/channel_arbitrator.go @@ -462,7 +462,9 @@ func (c *ChannelArbitrator) getStartState(tx kvdb.RTx) (*chanArbStartState, // Start starts all the goroutines that the ChannelArbitrator needs to operate. // If takes a start state, which will be looked up on disk if it is not // provided. -func (c *ChannelArbitrator) Start(state *chanArbStartState) error { +func (c *ChannelArbitrator) Start(state *chanArbStartState, + beat chainio.Blockbeat) error { + if !atomic.CompareAndSwapInt32(&c.started, 0, 1) { return nil } @@ -484,10 +486,8 @@ func (c *ChannelArbitrator) Start(state *chanArbStartState) error { // Set our state from our starting state. c.state = state.currentState - _, bestHeight, err := c.cfg.ChainIO.GetBestBlock() - if err != nil { - return err - } + // Get the starting height. + bestHeight := beat.Height() c.wg.Add(1) go c.channelAttendant(bestHeight, state.commitSet) diff --git a/contractcourt/channel_arbitrator_test.go b/contractcourt/channel_arbitrator_test.go index 8715e1e63de..a3bdf6ba3b9 100644 --- a/contractcourt/channel_arbitrator_test.go +++ b/contractcourt/channel_arbitrator_test.go @@ -296,7 +296,8 @@ func (c *chanArbTestCtx) Restart(restartClosure func(*chanArbTestCtx)) (*chanArb restartClosure(newCtx) } - if err := newCtx.chanArb.Start(nil); err != nil { + beat := newBeatFromHeight(0) + if err := newCtx.chanArb.Start(nil, beat); err != nil { return nil, err } @@ -523,7 +524,8 @@ func TestChannelArbitratorCooperativeClose(t *testing.T) { chanArbCtx, err := createTestChannelArbitrator(t, log) require.NoError(t, err, "unable to create ChannelArbitrator") - if err := chanArbCtx.chanArb.Start(nil); err != nil { + beat := newBeatFromHeight(0) + if err := chanArbCtx.chanArb.Start(nil, beat); err != nil { t.Fatalf("unable to start ChannelArbitrator: %v", err) } t.Cleanup(func() { @@ -581,7 +583,8 @@ func TestChannelArbitratorRemoteForceClose(t *testing.T) { require.NoError(t, err, "unable to create ChannelArbitrator") chanArb := chanArbCtx.chanArb - if err := chanArb.Start(nil); err != nil { + beat := newBeatFromHeight(0) + if err := chanArb.Start(nil, beat); err != nil { t.Fatalf("unable to start ChannelArbitrator: %v", err) } defer chanArb.Stop() @@ -634,7 +637,8 @@ func TestChannelArbitratorLocalForceClose(t *testing.T) { require.NoError(t, err, "unable to create ChannelArbitrator") chanArb := chanArbCtx.chanArb - if err := chanArb.Start(nil); err != nil { + beat := newBeatFromHeight(0) + if err := chanArb.Start(nil, beat); err != nil { t.Fatalf("unable to start ChannelArbitrator: %v", err) } defer chanArb.Stop() @@ -746,7 +750,8 @@ func TestChannelArbitratorBreachClose(t *testing.T) { chanArb.cfg.PreimageDB = newMockWitnessBeacon() chanArb.cfg.Registry = &mockRegistry{} - if err := chanArb.Start(nil); err != nil { + beat := newBeatFromHeight(0) + if err := chanArb.Start(nil, beat); err != nil { t.Fatalf("unable to start ChannelArbitrator: %v", err) } t.Cleanup(func() { @@ -873,7 +878,8 @@ func TestChannelArbitratorLocalForceClosePendingHtlc(t *testing.T) { chanArb.cfg.PreimageDB = newMockWitnessBeacon() chanArb.cfg.Registry = &mockRegistry{} - if err := chanArb.Start(nil); err != nil { + beat := newBeatFromHeight(0) + if err := chanArb.Start(nil, beat); err != nil { t.Fatalf("unable to start ChannelArbitrator: %v", err) } defer chanArb.Stop() @@ -1163,7 +1169,8 @@ func TestChannelArbitratorLocalForceCloseRemoteConfirmed(t *testing.T) { require.NoError(t, err, "unable to create ChannelArbitrator") chanArb := chanArbCtx.chanArb - if err := chanArb.Start(nil); err != nil { + beat := newBeatFromHeight(0) + if err := chanArb.Start(nil, beat); err != nil { t.Fatalf("unable to start ChannelArbitrator: %v", err) } defer chanArb.Stop() @@ -1270,7 +1277,8 @@ func TestChannelArbitratorLocalForceDoubleSpend(t *testing.T) { require.NoError(t, err, "unable to create ChannelArbitrator") chanArb := chanArbCtx.chanArb - if err := chanArb.Start(nil); err != nil { + beat := newBeatFromHeight(0) + if err := chanArb.Start(nil, beat); err != nil { t.Fatalf("unable to start ChannelArbitrator: %v", err) } defer chanArb.Stop() @@ -1376,7 +1384,8 @@ func TestChannelArbitratorPersistence(t *testing.T) { require.NoError(t, err, "unable to create ChannelArbitrator") chanArb := chanArbCtx.chanArb - if err := chanArb.Start(nil); err != nil { + beat := newBeatFromHeight(0) + if err := chanArb.Start(nil, beat); err != nil { t.Fatalf("unable to start ChannelArbitrator: %v", err) } @@ -1494,7 +1503,8 @@ func TestChannelArbitratorForceCloseBreachedChannel(t *testing.T) { require.NoError(t, err, "unable to create ChannelArbitrator") chanArb := chanArbCtx.chanArb - if err := chanArb.Start(nil); err != nil { + beat := newBeatFromHeight(0) + if err := chanArb.Start(nil, beat); err != nil { t.Fatalf("unable to start ChannelArbitrator: %v", err) } @@ -1681,7 +1691,8 @@ func TestChannelArbitratorCommitFailure(t *testing.T) { } chanArb := chanArbCtx.chanArb - if err := chanArb.Start(nil); err != nil { + beat := newBeatFromHeight(0) + if err := chanArb.Start(nil, beat); err != nil { t.Fatalf("unable to start ChannelArbitrator: %v", err) } @@ -1765,7 +1776,8 @@ func TestChannelArbitratorEmptyResolutions(t *testing.T) { chanArb.cfg.ClosingHeight = 100 chanArb.cfg.CloseType = channeldb.RemoteForceClose - if err := chanArb.Start(nil); err != nil { + beat := newBeatFromHeight(100) + if err := chanArb.Start(nil, beat); err != nil { t.Fatalf("unable to start ChannelArbitrator: %v", err) } @@ -1795,7 +1807,8 @@ func TestChannelArbitratorAlreadyForceClosed(t *testing.T) { chanArbCtx, err := createTestChannelArbitrator(t, log) require.NoError(t, err, "unable to create ChannelArbitrator") chanArb := chanArbCtx.chanArb - if err := chanArb.Start(nil); err != nil { + beat := newBeatFromHeight(0) + if err := chanArb.Start(nil, beat); err != nil { t.Fatalf("unable to start ChannelArbitrator: %v", err) } defer chanArb.Stop() @@ -1893,9 +1906,10 @@ func TestChannelArbitratorDanglingCommitForceClose(t *testing.T) { t.Fatalf("unable to create ChannelArbitrator: %v", err) } chanArb := chanArbCtx.chanArb - if err := chanArb.Start(nil); err != nil { - t.Fatalf("unable to start ChannelArbitrator: %v", err) - } + beat := newBeatFromHeight(0) + err = chanArb.Start(nil, beat) + require.NoError(t, err) + defer chanArb.Stop() // Now that our channel arb has started, we'll set up @@ -2089,7 +2103,8 @@ func TestChannelArbitratorPendingExpiredHTLC(t *testing.T) { return false } - if err := chanArb.Start(nil); err != nil { + beat := newBeatFromHeight(0) + if err := chanArb.Start(nil, beat); err != nil { t.Fatalf("unable to start ChannelArbitrator: %v", err) } t.Cleanup(func() { @@ -2123,7 +2138,7 @@ func TestChannelArbitratorPendingExpiredHTLC(t *testing.T) { // We will advance the uptime to 10 seconds which should be still within // the grace period and should not trigger going to chain. testClock.SetTime(startTime.Add(time.Second * 10)) - beat := newBeatFromHeight(5) + beat = newBeatFromHeight(5) chanArbCtx.chanArb.BlockbeatChan <- beat chanArbCtx.AssertState(StateDefault) @@ -2244,8 +2259,8 @@ func TestRemoteCloseInitiator(t *testing.T) { "ChannelArbitrator: %v", err) } chanArb := chanArbCtx.chanArb - - if err := chanArb.Start(nil); err != nil { + beat := newBeatFromHeight(0) + if err := chanArb.Start(nil, beat); err != nil { t.Fatalf("unable to start "+ "ChannelArbitrator: %v", err) } @@ -2796,7 +2811,9 @@ func TestChannelArbitratorAnchors(t *testing.T) { }, } - if err := chanArb.Start(nil); err != nil { + heightHint := uint32(1000) + beat := newBeatFromHeight(int32(heightHint)) + if err := chanArb.Start(nil, beat); err != nil { t.Fatalf("unable to start ChannelArbitrator: %v", err) } t.Cleanup(func() { @@ -2809,8 +2826,7 @@ func TestChannelArbitratorAnchors(t *testing.T) { chanArb.UpdateContractSignals(signals) // Set current block height. - heightHint := uint32(1000) - beat := newBeatFromHeight(int32(heightHint)) + beat = newBeatFromHeight(int32(heightHint)) chanArbCtx.chanArb.BlockbeatChan <- beat htlcAmt := lnwire.MilliSatoshi(1_000_000) @@ -3089,7 +3105,8 @@ func TestChannelArbitratorStartForceCloseFail(t *testing.T) { return test.broadcastErr } - err = chanArb.Start(nil) + beat := newBeatFromHeight(0) + err = chanArb.Start(nil, beat) if !test.expectedStartup { require.ErrorIs(t, err, test.broadcastErr) From 545cea0546920965728f31a6680c903dc4510362 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 29 Oct 2024 22:02:13 +0800 Subject: [PATCH 26/92] multi: start consumers with a starting blockbeat This is needed so the consumers have an initial state about the current block. --- contractcourt/chain_arbitrator.go | 9 ++++-- contractcourt/chain_arbitrator_test.go | 6 ++-- server.go | 45 ++++++++++++++++++++++++-- sweep/fee_bumper.go | 5 ++- sweep/sweeper.go | 5 ++- 5 files changed, 60 insertions(+), 10 deletions(-) diff --git a/contractcourt/chain_arbitrator.go b/contractcourt/chain_arbitrator.go index 34ed7d7ff55..524958f48ff 100644 --- a/contractcourt/chain_arbitrator.go +++ b/contractcourt/chain_arbitrator.go @@ -570,13 +570,16 @@ func (c *ChainArbitrator) ResolveContract(chanPoint wire.OutPoint) error { } // Start launches all goroutines that the ChainArbitrator needs to operate. -func (c *ChainArbitrator) Start() error { +func (c *ChainArbitrator) Start(beat chainio.Blockbeat) error { if !atomic.CompareAndSwapInt32(&c.started, 0, 1) { return nil } - log.Infof("ChainArbitrator starting with config: budget=[%v]", - &c.cfg.Budget) + // Set the current beat. + c.beat = beat + + log.Infof("ChainArbitrator starting at height %d with budget=[%v]", + &c.cfg.Budget, c.beat.Height()) // First, we'll fetch all the channels that are still open, in order to // collect them within our set of active contracts. diff --git a/contractcourt/chain_arbitrator_test.go b/contractcourt/chain_arbitrator_test.go index de6d69900b4..622686f76c4 100644 --- a/contractcourt/chain_arbitrator_test.go +++ b/contractcourt/chain_arbitrator_test.go @@ -90,7 +90,8 @@ func TestChainArbitratorRepublishCloses(t *testing.T) { chainArbCfg, db, ) - if err := chainArb.Start(); err != nil { + beat := newBeatFromHeight(0) + if err := chainArb.Start(beat); err != nil { t.Fatal(err) } t.Cleanup(func() { @@ -173,7 +174,8 @@ func TestResolveContract(t *testing.T) { chainArb := NewChainArbitrator( chainArbCfg, db, ) - if err := chainArb.Start(); err != nil { + beat := newBeatFromHeight(0) + if err := chainArb.Start(beat); err != nil { t.Fatal(err) } t.Cleanup(func() { diff --git a/server.go b/server.go index 799bfde6b87..f83333bc689 100644 --- a/server.go +++ b/server.go @@ -28,6 +28,7 @@ import ( "github.com/lightningnetwork/lnd/aliasmgr" "github.com/lightningnetwork/lnd/autopilot" "github.com/lightningnetwork/lnd/brontide" + "github.com/lightningnetwork/lnd/chainio" "github.com/lightningnetwork/lnd/chainreg" "github.com/lightningnetwork/lnd/chanacceptor" "github.com/lightningnetwork/lnd/chanbackup" @@ -2073,6 +2074,12 @@ func (c cleaner) run() { // //nolint:funlen func (s *server) Start() error { + // Get the current blockbeat. + beat, err := s.getStartingBeat() + if err != nil { + return err + } + var startErr error // If one sub system fails to start, the following code ensures that the @@ -2167,13 +2174,13 @@ func (s *server) Start() error { } cleanup = cleanup.add(s.txPublisher.Stop) - if err := s.txPublisher.Start(); err != nil { + if err := s.txPublisher.Start(beat); err != nil { startErr = err return } cleanup = cleanup.add(s.sweeper.Stop) - if err := s.sweeper.Start(); err != nil { + if err := s.sweeper.Start(beat); err != nil { startErr = err return } @@ -2218,7 +2225,7 @@ func (s *server) Start() error { } cleanup = cleanup.add(s.chainArb.Stop) - if err := s.chainArb.Start(); err != nil { + if err := s.chainArb.Start(beat); err != nil { startErr = err return } @@ -5152,3 +5159,35 @@ func (s *server) fetchClosedChannelSCIDs() map[lnwire.ShortChannelID]struct{} { return closedSCIDs } + +// getStartingBeat returns the current beat. This is used during the startup to +// initialize blockbeat consumers. +func (s *server) getStartingBeat() (*chainio.Beat, error) { + // beat is the current blockbeat. + var beat *chainio.Beat + + // We should get a notification with the current best block immediately + // by passing a nil block. + blockEpochs, err := s.cc.ChainNotifier.RegisterBlockEpochNtfn(nil) + if err != nil { + return beat, fmt.Errorf("register block epoch ntfn: %w", err) + } + defer blockEpochs.Cancel() + + // We registered for the block epochs with a nil request. The notifier + // should send us the current best block immediately. So we need to + // wait for it here because we need to know the current best height. + select { + case bestBlock := <-blockEpochs.Epochs: + srvrLog.Infof("Received initial block %v at height %d", + bestBlock.Hash, bestBlock.Height) + + // Update the current blockbeat. + beat = chainio.NewBeat(*bestBlock) + + case <-s.quit: + srvrLog.Debug("LND shutting down") + } + + return beat, nil +} diff --git a/sweep/fee_bumper.go b/sweep/fee_bumper.go index ad02ac61946..a43f8758508 100644 --- a/sweep/fee_bumper.go +++ b/sweep/fee_bumper.go @@ -795,13 +795,16 @@ type monitorRecord struct { // Start starts the publisher by subscribing to block epoch updates and kicking // off the monitor loop. -func (t *TxPublisher) Start() error { +func (t *TxPublisher) Start(beat chainio.Blockbeat) error { log.Info("TxPublisher starting...") if t.started.Swap(true) { return fmt.Errorf("TxPublisher started more than once") } + // Set the current height. + t.currentHeight.Store(beat.Height()) + t.wg.Add(1) go t.monitor() diff --git a/sweep/sweeper.go b/sweep/sweeper.go index b91a6808fbf..0f2675e8f81 100644 --- a/sweep/sweeper.go +++ b/sweep/sweeper.go @@ -441,7 +441,7 @@ func New(cfg *UtxoSweeperConfig) *UtxoSweeper { } // Start starts the process of constructing and publish sweep txes. -func (s *UtxoSweeper) Start() error { +func (s *UtxoSweeper) Start(beat chainio.Blockbeat) error { if !atomic.CompareAndSwapUint32(&s.started, 0, 1) { return nil } @@ -452,6 +452,9 @@ func (s *UtxoSweeper) Start() error { // not change from here on. s.relayFeeRate = s.cfg.FeeEstimator.RelayFeePerKW() + // Set the current height. + s.currentHeight = beat.Height() + // Start sweeper main loop. s.wg.Add(1) go s.collector() From 16a8b623b38fc92a2f71a8247a987f4e14412011 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 17 Oct 2024 09:58:48 +0800 Subject: [PATCH 27/92] lnd: add new method `startLowLevelServices` In this commit we start to break up the starting process into smaller pieces, which is needed in the following commit to initialize blockbeat consumers. --- server.go | 40 ++++++++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/server.go b/server.go index f83333bc689..5b64ade348b 100644 --- a/server.go +++ b/server.go @@ -666,6 +666,17 @@ func newServer(cfg *Config, listenAddrs []net.Addr, quit: make(chan struct{}), } + // Start the low-level services once they are initialized. + // + // TODO(yy): break the server startup into four steps, + // 1. init the low-level services. + // 2. start the low-level services. + // 3. init the high-level services. + // 4. start the high-level services. + if err := s.startLowLevelServices(); err != nil { + return nil, err + } + currentHash, currentHeight, err := s.cc.ChainIO.GetBestBlock() if err != nil { return nil, err @@ -2068,6 +2079,29 @@ func (c cleaner) run() { } } +// startLowLevelServices starts the low-level services of the server. These +// services must be started successfully before running the main server. The +// services are, +// 1. the chain notifier. +// +// TODO(yy): identify and add more low-level services here. +func (s *server) startLowLevelServices() error { + var startErr error + + cleanup := cleaner{} + + cleanup = cleanup.add(s.cc.ChainNotifier.Stop) + if err := s.cc.ChainNotifier.Start(); err != nil { + startErr = err + } + + if startErr != nil { + cleanup.run() + } + + return startErr +} + // Start starts the main daemon server, all requested listeners, and any helper // goroutines. // NOTE: This function is safe for concurrent access. @@ -2133,12 +2167,6 @@ func (s *server) Start() error { return } - cleanup = cleanup.add(s.cc.ChainNotifier.Stop) - if err := s.cc.ChainNotifier.Start(); err != nil { - startErr = err - return - } - cleanup = cleanup.add(s.cc.BestBlockTracker.Stop) if err := s.cc.BestBlockTracker.Start(); err != nil { startErr = err From 8fc915450642df90ce3cfb76d40056f3a8a77cc3 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 17 Oct 2024 10:14:00 +0800 Subject: [PATCH 28/92] lnd: start `blockbeatDispatcher` and register consumers --- log.go | 2 ++ server.go | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/log.go b/log.go index ae2d2d811f1..d3c93b43251 100644 --- a/log.go +++ b/log.go @@ -9,6 +9,7 @@ import ( sphinx "github.com/lightningnetwork/lightning-onion" "github.com/lightningnetwork/lnd/autopilot" "github.com/lightningnetwork/lnd/build" + "github.com/lightningnetwork/lnd/chainio" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/chainreg" "github.com/lightningnetwork/lnd/chanacceptor" @@ -198,6 +199,7 @@ func SetupLoggers(root *build.SubLoggerManager, interceptor signal.Interceptor) root, blindedpath.Subsystem, interceptor, blindedpath.UseLogger, ) AddV1SubLogger(root, graphdb.Subsystem, interceptor, graphdb.UseLogger) + AddSubLogger(root, chainio.Subsystem, interceptor, chainio.UseLogger) } // AddSubLogger is a helper method to conveniently create and register the diff --git a/server.go b/server.go index 5b64ade348b..9fd0b7a006b 100644 --- a/server.go +++ b/server.go @@ -357,6 +357,10 @@ type server struct { // txPublisher is a publisher with fee-bumping capability. txPublisher *sweep.TxPublisher + // blockbeatDispatcher is a block dispatcher that notifies subscribers + // of new blocks. + blockbeatDispatcher *chainio.BlockbeatDispatcher + quit chan struct{} wg sync.WaitGroup @@ -624,6 +628,9 @@ func newServer(cfg *Config, listenAddrs []net.Addr, readPool: readPool, chansToRestore: chansToRestore, + blockbeatDispatcher: chainio.NewBlockbeatDispatcher( + cc.ChainNotifier, + ), channelNotifier: channelnotifier.New( dbs.ChanStateDB.ChannelStateDB(), ), @@ -1825,6 +1832,9 @@ func newServer(cfg *Config, listenAddrs []net.Addr, } s.connMgr = cmgr + // Finally, register the subsystems in blockbeat. + s.registerBlockConsumers() + return s, nil } @@ -1857,6 +1867,25 @@ func (s *server) UpdateRoutingConfig(cfg *routing.MissionControlConfig) { routerCfg.MaxMcHistory = cfg.MaxMcHistory } +// registerBlockConsumers registers the subsystems that consume block events. +// By calling `RegisterQueue`, a list of subsystems are registered in the +// blockbeat for block notifications. When a new block arrives, the subsystems +// in the same queue are notified sequentially, and different queues are +// notified concurrently. +// +// NOTE: To put a subsystem in a different queue, create a slice and pass it to +// a new `RegisterQueue` call. +func (s *server) registerBlockConsumers() { + // In this queue, when a new block arrives, it will be received and + // processed in this order: chainArb -> sweeper -> txPublisher. + consumers := []chainio.Consumer{ + s.chainArb, + s.sweeper, + s.txPublisher, + } + s.blockbeatDispatcher.RegisterQueue(consumers) +} + // signAliasUpdate takes a ChannelUpdate and returns the signature. This is // used for option_scid_alias channels where the ChannelUpdate to be sent back // may differ from what is on disk. @@ -2494,6 +2523,17 @@ func (s *server) Start() error { srvrLog.Infof("Auto peer bootstrapping is disabled") } + // Start the blockbeat after all other subsystems have been + // started so they are ready to receive new blocks. + cleanup = cleanup.add(func() error { + s.blockbeatDispatcher.Stop() + return nil + }) + if err := s.blockbeatDispatcher.Start(); err != nil { + startErr = err + return + } + // Set the active flag now that we've completed the full // startup. atomic.StoreInt32(&s.active, 1) @@ -2518,6 +2558,9 @@ func (s *server) Stop() error { // Shutdown connMgr first to prevent conns during shutdown. s.connMgr.Stop() + // Stop dispatching blocks to other systems immediately. + s.blockbeatDispatcher.Stop() + // Shutdown the wallet, funding manager, and the rpc server. if err := s.chanStatusMgr.Stop(); err != nil { srvrLog.Warnf("failed to stop chanStatusMgr: %v", err) From 45b243c91c9196ac0bdac09cab2e9fcf0c91dbf4 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Wed, 30 Oct 2024 04:38:27 +0800 Subject: [PATCH 29/92] contractcourt: fix linter `funlen` Refactor the `Start` method to fix the linter error: ``` contractcourt/chain_arbitrator.go:568: Function 'Start' is too long (242 > 200) (funlen) ``` --- contractcourt/chain_arbitrator.go | 270 ++++++++++++++----------- contractcourt/commit_sweep_resolver.go | 4 + 2 files changed, 151 insertions(+), 123 deletions(-) diff --git a/contractcourt/chain_arbitrator.go b/contractcourt/chain_arbitrator.go index 524958f48ff..011b5225cde 100644 --- a/contractcourt/chain_arbitrator.go +++ b/contractcourt/chain_arbitrator.go @@ -583,137 +583,17 @@ func (c *ChainArbitrator) Start(beat chainio.Blockbeat) error { // First, we'll fetch all the channels that are still open, in order to // collect them within our set of active contracts. - openChannels, err := c.chanSource.ChannelStateDB().FetchAllChannels() - if err != nil { + if err := c.loadOpenChannels(); err != nil { return err } - if len(openChannels) > 0 { - log.Infof("Creating ChannelArbitrators for %v active channels", - len(openChannels)) - } - - // For each open channel, we'll configure then launch a corresponding - // ChannelArbitrator. - for _, channel := range openChannels { - chanPoint := channel.FundingOutpoint - channel := channel - - // First, we'll create an active chainWatcher for this channel - // to ensure that we detect any relevant on chain events. - breachClosure := func(ret *lnwallet.BreachRetribution) error { - return c.cfg.ContractBreach(chanPoint, ret) - } - - chainWatcher, err := newChainWatcher( - chainWatcherConfig{ - chanState: channel, - notifier: c.cfg.Notifier, - signer: c.cfg.Signer, - isOurAddr: c.cfg.IsOurAddress, - contractBreach: breachClosure, - extractStateNumHint: lnwallet.GetStateNumHint, - auxLeafStore: c.cfg.AuxLeafStore, - auxResolver: c.cfg.AuxResolver, - }, - ) - if err != nil { - return err - } - - c.activeWatchers[chanPoint] = chainWatcher - channelArb, err := newActiveChannelArbitrator( - channel, c, chainWatcher.SubscribeChannelEvents(), - ) - if err != nil { - return err - } - - c.activeChannels[chanPoint] = channelArb - - // Republish any closing transactions for this channel. - err = c.republishClosingTxs(channel) - if err != nil { - log.Errorf("Failed to republish closing txs for "+ - "channel %v", chanPoint) - } - } - // In addition to the channels that we know to be open, we'll also // launch arbitrators to finishing resolving any channels that are in // the pending close state. - closingChannels, err := c.chanSource.ChannelStateDB().FetchClosedChannels( - true, - ) - if err != nil { + if err := c.loadPendingCloseChannels(); err != nil { return err } - if len(closingChannels) > 0 { - log.Infof("Creating ChannelArbitrators for %v closing channels", - len(closingChannels)) - } - - // Next, for each channel is the closing state, we'll launch a - // corresponding more restricted resolver, as we don't have to watch - // the chain any longer, only resolve the contracts on the confirmed - // commitment. - //nolint:ll - for _, closeChanInfo := range closingChannels { - // We can leave off the CloseContract and ForceCloseChan - // methods as the channel is already closed at this point. - chanPoint := closeChanInfo.ChanPoint - arbCfg := ChannelArbitratorConfig{ - ChanPoint: chanPoint, - ShortChanID: closeChanInfo.ShortChanID, - ChainArbitratorConfig: c.cfg, - ChainEvents: &ChainEventSubscription{}, - IsPendingClose: true, - ClosingHeight: closeChanInfo.CloseHeight, - CloseType: closeChanInfo.CloseType, - PutResolverReport: func(tx kvdb.RwTx, - report *channeldb.ResolverReport) error { - - return c.chanSource.PutResolverReport( - tx, c.cfg.ChainHash, &chanPoint, report, - ) - }, - FetchHistoricalChannel: func() (*channeldb.OpenChannel, error) { - chanStateDB := c.chanSource.ChannelStateDB() - return chanStateDB.FetchHistoricalChannel(&chanPoint) - }, - FindOutgoingHTLCDeadline: func( - htlc channeldb.HTLC) fn.Option[int32] { - - return c.FindOutgoingHTLCDeadline( - closeChanInfo.ShortChanID, htlc, - ) - }, - } - chanLog, err := newBoltArbitratorLog( - c.chanSource.Backend, arbCfg, c.cfg.ChainHash, chanPoint, - ) - if err != nil { - return err - } - arbCfg.MarkChannelResolved = func() error { - if c.cfg.NotifyFullyResolvedChannel != nil { - c.cfg.NotifyFullyResolvedChannel(chanPoint) - } - - return c.ResolveContract(chanPoint) - } - - // We create an empty map of HTLC's here since it's possible - // that the channel is in StateDefault and updateActiveHTLCs is - // called. We want to avoid writing to an empty map. Since the - // channel is already in the process of being resolved, no new - // HTLCs will be added. - c.activeChannels[chanPoint] = NewChannelArbitrator( - arbCfg, make(map[HtlcSetKey]htlcSet), chanLog, - ) - } - // Now, we'll start all chain watchers in parallel to shorten start up // duration. In neutrino mode, this allows spend registrations to take // advantage of batch spend reporting, instead of doing a single rescan @@ -765,7 +645,7 @@ func (c *ChainArbitrator) Start(beat chainio.Blockbeat) error { // transaction. var startStates map[wire.OutPoint]*chanArbStartState - err = kvdb.View(c.chanSource, func(tx walletdb.ReadTx) error { + err := kvdb.View(c.chanSource, func(tx walletdb.ReadTx) error { for _, arbitrator := range c.activeChannels { startState, err := arbitrator.getStartState(tx) if err != nil { @@ -1332,3 +1212,147 @@ func (c *ChainArbitrator) FindOutgoingHTLCDeadline(scid lnwire.ShortChannelID, func (c *ChainArbitrator) Name() string { return "ChainArbitrator" } + +// loadOpenChannels loads all channels that are currently open in the database +// and registers them with the chainWatcher for future notification. +func (c *ChainArbitrator) loadOpenChannels() error { + openChannels, err := c.chanSource.ChannelStateDB().FetchAllChannels() + if err != nil { + return err + } + + if len(openChannels) == 0 { + return nil + } + + log.Infof("Creating ChannelArbitrators for %v active channels", + len(openChannels)) + + // For each open channel, we'll configure then launch a corresponding + // ChannelArbitrator. + for _, channel := range openChannels { + chanPoint := channel.FundingOutpoint + channel := channel + + // First, we'll create an active chainWatcher for this channel + // to ensure that we detect any relevant on chain events. + breachClosure := func(ret *lnwallet.BreachRetribution) error { + return c.cfg.ContractBreach(chanPoint, ret) + } + + chainWatcher, err := newChainWatcher( + chainWatcherConfig{ + chanState: channel, + notifier: c.cfg.Notifier, + signer: c.cfg.Signer, + isOurAddr: c.cfg.IsOurAddress, + contractBreach: breachClosure, + extractStateNumHint: lnwallet.GetStateNumHint, + auxLeafStore: c.cfg.AuxLeafStore, + auxResolver: c.cfg.AuxResolver, + }, + ) + if err != nil { + return err + } + + c.activeWatchers[chanPoint] = chainWatcher + channelArb, err := newActiveChannelArbitrator( + channel, c, chainWatcher.SubscribeChannelEvents(), + ) + if err != nil { + return err + } + + c.activeChannels[chanPoint] = channelArb + + // Republish any closing transactions for this channel. + err = c.republishClosingTxs(channel) + if err != nil { + log.Errorf("Failed to republish closing txs for "+ + "channel %v", chanPoint) + } + } + + return nil +} + +// loadPendingCloseChannels loads all channels that are currently pending +// closure in the database and registers them with the ChannelArbitrator to +// continue the resolution process. +func (c *ChainArbitrator) loadPendingCloseChannels() error { + chanStateDB := c.chanSource.ChannelStateDB() + + closingChannels, err := chanStateDB.FetchClosedChannels(true) + if err != nil { + return err + } + + if len(closingChannels) == 0 { + return nil + } + + log.Infof("Creating ChannelArbitrators for %v closing channels", + len(closingChannels)) + + // Next, for each channel is the closing state, we'll launch a + // corresponding more restricted resolver, as we don't have to watch + // the chain any longer, only resolve the contracts on the confirmed + // commitment. + //nolint:ll + for _, closeChanInfo := range closingChannels { + // We can leave off the CloseContract and ForceCloseChan + // methods as the channel is already closed at this point. + chanPoint := closeChanInfo.ChanPoint + arbCfg := ChannelArbitratorConfig{ + ChanPoint: chanPoint, + ShortChanID: closeChanInfo.ShortChanID, + ChainArbitratorConfig: c.cfg, + ChainEvents: &ChainEventSubscription{}, + IsPendingClose: true, + ClosingHeight: closeChanInfo.CloseHeight, + CloseType: closeChanInfo.CloseType, + PutResolverReport: func(tx kvdb.RwTx, + report *channeldb.ResolverReport) error { + + return c.chanSource.PutResolverReport( + tx, c.cfg.ChainHash, &chanPoint, report, + ) + }, + FetchHistoricalChannel: func() (*channeldb.OpenChannel, error) { + return chanStateDB.FetchHistoricalChannel(&chanPoint) + }, + FindOutgoingHTLCDeadline: func( + htlc channeldb.HTLC) fn.Option[int32] { + + return c.FindOutgoingHTLCDeadline( + closeChanInfo.ShortChanID, htlc, + ) + }, + } + chanLog, err := newBoltArbitratorLog( + c.chanSource.Backend, arbCfg, c.cfg.ChainHash, chanPoint, + ) + if err != nil { + return err + } + arbCfg.MarkChannelResolved = func() error { + if c.cfg.NotifyFullyResolvedChannel != nil { + c.cfg.NotifyFullyResolvedChannel(chanPoint) + } + + return c.ResolveContract(chanPoint) + } + + // We create an empty map of HTLC's here since it's possible + // that the channel is in StateDefault and updateActiveHTLCs is + // called. We want to avoid writing to an empty map. Since the + // channel is already in the process of being resolved, no new + // HTLCs will be added. + c.activeChannels[chanPoint] = NewChannelArbitrator( + arbCfg, make(map[HtlcSetKey]htlcSet), chanLog, + ) + } + + return nil +} diff --git a/contractcourt/commit_sweep_resolver.go b/contractcourt/commit_sweep_resolver.go index 9a3bb058ae3..423d235dbf7 100644 --- a/contractcourt/commit_sweep_resolver.go +++ b/contractcourt/commit_sweep_resolver.go @@ -165,6 +165,10 @@ func (c *commitSweepResolver) getCommitTxConfHeight() (uint32, error) { // returned. // // NOTE: This function MUST be run as a goroutine. + +// TODO(yy): fix the funlen in the next PR. +// +//nolint:funlen func (c *commitSweepResolver) Resolve() (ContractResolver, error) { // If we're already resolved, then we can exit early. if c.resolved { From 1d53e7d0813ae3d0ea4a09c2eaf41fcc7b77a24d Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Wed, 22 May 2024 16:51:36 +0800 Subject: [PATCH 30/92] multi: improve loggings --- contractcourt/channel_arbitrator.go | 12 +++++++----- contractcourt/htlc_lease_resolver.go | 6 +++--- contractcourt/utxonursery.go | 2 +- htlcswitch/switch.go | 2 +- lnwallet/wallet.go | 2 +- sweep/sweeper.go | 16 ++++++++++------ 6 files changed, 23 insertions(+), 17 deletions(-) diff --git a/contractcourt/channel_arbitrator.go b/contractcourt/channel_arbitrator.go index 85f082111a8..b94adf51f3c 100644 --- a/contractcourt/channel_arbitrator.go +++ b/contractcourt/channel_arbitrator.go @@ -1610,8 +1610,8 @@ func (c *ChannelArbitrator) advanceState( for { priorState = c.state log.Debugf("ChannelArbitrator(%v): attempting state step with "+ - "trigger=%v from state=%v", c.cfg.ChanPoint, trigger, - priorState) + "trigger=%v from state=%v at height=%v", + c.cfg.ChanPoint, trigger, priorState, triggerHeight) nextState, closeTx, err := c.stateStep( triggerHeight, trigger, confCommitSet, @@ -2822,14 +2822,12 @@ func (c *ChannelArbitrator) channelAttendant(bestHeight int32, // We have broadcasted our commitment, and it is now confirmed // on-chain. case closeInfo := <-c.cfg.ChainEvents.LocalUnilateralClosure: - log.Infof("ChannelArbitrator(%v): local on-chain "+ - "channel close", c.cfg.ChanPoint) - if c.state != StateCommitmentBroadcasted { log.Errorf("ChannelArbitrator(%v): unexpected "+ "local on-chain channel close", c.cfg.ChanPoint) } + closeTx := closeInfo.CloseTx resolutions, err := closeInfo.ContractResolutions. @@ -2857,6 +2855,10 @@ func (c *ChannelArbitrator) channelAttendant(bestHeight int32, return } + log.Infof("ChannelArbitrator(%v): local force close "+ + "tx=%v confirmed", c.cfg.ChanPoint, + closeTx.TxHash()) + contractRes := &ContractResolutions{ CommitHash: closeTx.TxHash(), CommitResolution: resolutions.CommitResolution, diff --git a/contractcourt/htlc_lease_resolver.go b/contractcourt/htlc_lease_resolver.go index 9c5da6ee496..6230f96777b 100644 --- a/contractcourt/htlc_lease_resolver.go +++ b/contractcourt/htlc_lease_resolver.go @@ -57,10 +57,10 @@ func (h *htlcLeaseResolver) makeSweepInput(op *wire.OutPoint, signDesc *input.SignDescriptor, csvDelay, broadcastHeight uint32, payHash [32]byte, resBlob fn.Option[tlv.Blob]) *input.BaseInput { - if h.hasCLTV() { - log.Infof("%T(%x): CSV and CLTV locks expired, offering "+ - "second-layer output to sweeper: %v", h, payHash, op) + log.Infof("%T(%x): offering second-layer output to sweeper: %v", h, + payHash, op) + if h.hasCLTV() { return input.NewCsvInputWithCltv( op, cltvWtype, signDesc, broadcastHeight, csvDelay, diff --git a/contractcourt/utxonursery.go b/contractcourt/utxonursery.go index a870683746e..f78be9fa494 100644 --- a/contractcourt/utxonursery.go +++ b/contractcourt/utxonursery.go @@ -794,7 +794,7 @@ func (u *UtxoNursery) graduateClass(classHeight uint32) error { return err } - utxnLog.Infof("Attempting to graduate height=%v: num_kids=%v, "+ + utxnLog.Debugf("Attempting to graduate height=%v: num_kids=%v, "+ "num_babies=%v", classHeight, len(kgtnOutputs), len(cribOutputs)) // Offer the outputs to the sweeper and set up notifications that will diff --git a/htlcswitch/switch.go b/htlcswitch/switch.go index 704887e8e12..720625f2c5a 100644 --- a/htlcswitch/switch.go +++ b/htlcswitch/switch.go @@ -1606,7 +1606,7 @@ out: } } - log.Infof("Received outside contract resolution, "+ + log.Debugf("Received outside contract resolution, "+ "mapping to: %v", spew.Sdump(pkt)) // We don't check the error, as the only failure we can diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index 96ea85cf9e2..2646d7c8f00 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -733,7 +733,7 @@ func (l *LightningWallet) RegisterFundingIntent(expectedID [32]byte, } if _, ok := l.fundingIntents[expectedID]; ok { - return fmt.Errorf("%w: already has intent registered: %v", + return fmt.Errorf("%w: already has intent registered: %x", ErrDuplicatePendingChanID, expectedID[:]) } diff --git a/sweep/sweeper.go b/sweep/sweeper.go index 0f2675e8f81..d49f104af06 100644 --- a/sweep/sweeper.go +++ b/sweep/sweeper.go @@ -242,8 +242,9 @@ func (p *SweeperInput) isMature(currentHeight uint32) (bool, uint32) { // currentHeight plus one. locktime = p.BlocksToMaturity() + p.HeightHint() if currentHeight+1 < locktime { - log.Debugf("Input %v has CSV expiry=%v, current height is %v", - p.OutPoint(), locktime, currentHeight) + log.Debugf("Input %v has CSV expiry=%v, current height is %v, "+ + "skipped sweeping", p.OutPoint(), locktime, + currentHeight) return false, locktime } @@ -1197,8 +1198,8 @@ func (s *UtxoSweeper) calculateDefaultDeadline(pi *SweeperInput) int32 { if !matured { defaultDeadline = int32(locktime + s.cfg.NoDeadlineConfTarget) log.Debugf("Input %v is immature, using locktime=%v instead "+ - "of current height=%d", pi.OutPoint(), locktime, - s.currentHeight) + "of current height=%d as starting height", + pi.OutPoint(), locktime, s.currentHeight) } return defaultDeadline @@ -1210,7 +1211,8 @@ func (s *UtxoSweeper) handleNewInput(input *sweepInputMessage) error { outpoint := input.input.OutPoint() pi, pending := s.inputs[outpoint] if pending { - log.Debugf("Already has pending input %v received", outpoint) + log.Infof("Already has pending input %v received, old params: "+ + "%v, new params %v", outpoint, pi.params, input.params) s.handleExistingInput(input, pi) @@ -1492,6 +1494,8 @@ func (s *UtxoSweeper) updateSweeperInputs() InputsMap { // turn this inputs map into a SyncMap in case we wanna add concurrent // access to the map in the future. for op, input := range s.inputs { + log.Tracef("Checking input: %s, state=%v", input, input.state) + // If the input has reached a final state, that it's either // been swept, or failed, or excluded, we will remove it from // our sweeper. @@ -1521,7 +1525,7 @@ func (s *UtxoSweeper) updateSweeperInputs() InputsMap { // skip this input and wait for the locktime to be reached. mature, locktime := input.isMature(uint32(s.currentHeight)) if !mature { - log.Infof("Skipping input %v due to locktime=%v not "+ + log.Debugf("Skipping input %v due to locktime=%v not "+ "reached, current height is %v", op, locktime, s.currentHeight) From 0bab6b34192b39556ca0c02cfcbabd06cc8232b3 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 19 Nov 2024 17:34:09 +0800 Subject: [PATCH 31/92] chainio: use `errgroup` to limit num of goroutines --- chainio/dispatcher.go | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/chainio/dispatcher.go b/chainio/dispatcher.go index 244a3ac8f73..87bc21fbaac 100644 --- a/chainio/dispatcher.go +++ b/chainio/dispatcher.go @@ -9,6 +9,7 @@ import ( "github.com/btcsuite/btclog/v2" "github.com/lightningnetwork/lnd/chainntnfs" + "golang.org/x/sync/errgroup" ) // DefaultProcessBlockTimeout is the timeout value used when waiting for one @@ -229,34 +230,30 @@ func DispatchSequential(b Blockbeat, consumers []Consumer) error { // It requires the consumer to finish processing the block within the specified // time, otherwise a timeout error is returned. func DispatchConcurrent(b Blockbeat, consumers []Consumer) error { - // errChans is a map of channels that will be used to receive errors - // returned from notifying the consumers. - errChans := make(map[string]chan error, len(consumers)) + eg := &errgroup.Group{} // Notify each queue in goroutines. for _, c := range consumers { - // Create a signal chan. - errChan := make(chan error, 1) - errChans[c.Name()] = errChan - // Notify each consumer concurrently. - go func(c Consumer, beat Blockbeat) { + eg.Go(func() error { // Send the beat to the consumer. - errChan <- notifyAndWait( - b, c, DefaultProcessBlockTimeout, - ) - }(c, b) - } + err := notifyAndWait(b, c, DefaultProcessBlockTimeout) + + // Exit early if there's no error. + if err == nil { + return nil + } - // Wait for all consumers in each queue to finish. - for name, errChan := range errChans { - err := <-errChan - if err != nil { b.logger().Errorf("Consumer=%v failed to process "+ - "block: %v", name, err) + "block: %v", c.Name(), err) return err - } + }) + } + + // Wait for all consumers in each queue to finish. + if err := eg.Wait(); err != nil { + return err } return nil From 1f2cfc6a60bcbc02c3179359ef9977f29e2d6671 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 20 Jun 2024 21:56:52 +0800 Subject: [PATCH 32/92] contractcourt: add verbose logging in resolvers We now put the outpoint in the resolvers's logging so it's easier to debug. --- contractcourt/anchor_resolver.go | 3 ++- contractcourt/breach_resolver.go | 5 +++-- contractcourt/commit_sweep_resolver.go | 4 ++-- contractcourt/contract_resolver.go | 6 ++++-- contractcourt/htlc_success_resolver.go | 25 +++++++++++++++---------- contractcourt/htlc_timeout_resolver.go | 24 ++++++++++++++---------- 6 files changed, 40 insertions(+), 27 deletions(-) diff --git a/contractcourt/anchor_resolver.go b/contractcourt/anchor_resolver.go index af7ac76462a..b50e061f447 100644 --- a/contractcourt/anchor_resolver.go +++ b/contractcourt/anchor_resolver.go @@ -2,6 +2,7 @@ package contractcourt import ( "errors" + "fmt" "io" "sync" @@ -71,7 +72,7 @@ func newAnchorResolver(anchorSignDescriptor input.SignDescriptor, currentReport: report, } - r.initLogger(r) + r.initLogger(fmt.Sprintf("%T(%v)", r, r.anchor)) return r } diff --git a/contractcourt/breach_resolver.go b/contractcourt/breach_resolver.go index 63395651cc3..9a5f4bbe08e 100644 --- a/contractcourt/breach_resolver.go +++ b/contractcourt/breach_resolver.go @@ -2,6 +2,7 @@ package contractcourt import ( "encoding/binary" + "fmt" "io" "github.com/lightningnetwork/lnd/channeldb" @@ -32,7 +33,7 @@ func newBreachResolver(resCfg ResolverConfig) *breachResolver { replyChan: make(chan struct{}), } - r.initLogger(r) + r.initLogger(fmt.Sprintf("%T(%v)", r, r.ChanPoint)) return r } @@ -114,7 +115,7 @@ func newBreachResolverFromReader(r io.Reader, resCfg ResolverConfig) ( return nil, err } - b.initLogger(b) + b.initLogger(fmt.Sprintf("%T(%v)", b, b.ChanPoint)) return b, nil } diff --git a/contractcourt/commit_sweep_resolver.go b/contractcourt/commit_sweep_resolver.go index 423d235dbf7..7b101f80e33 100644 --- a/contractcourt/commit_sweep_resolver.go +++ b/contractcourt/commit_sweep_resolver.go @@ -88,7 +88,7 @@ func newCommitSweepResolver(res lnwallet.CommitOutputResolution, chanPoint: chanPoint, } - r.initLogger(r) + r.initLogger(fmt.Sprintf("%T(%v)", r, r.commitResolution.SelfOutPoint)) r.initReport() return r @@ -484,7 +484,7 @@ func newCommitSweepResolverFromReader(r io.Reader, resCfg ResolverConfig) ( // removed this, but keep in mind that this data may still be present in // the database. - c.initLogger(c) + c.initLogger(fmt.Sprintf("%T(%v)", c, c.commitResolution.SelfOutPoint)) c.initReport() return c, nil diff --git a/contractcourt/contract_resolver.go b/contractcourt/contract_resolver.go index 3629c1bc3c6..ff52ce9760d 100644 --- a/contractcourt/contract_resolver.go +++ b/contractcourt/contract_resolver.go @@ -120,8 +120,10 @@ func newContractResolverKit(cfg ResolverConfig) *contractResolverKit { } // initLogger initializes the resolver-specific logger. -func (r *contractResolverKit) initLogger(resolver ContractResolver) { - logPrefix := fmt.Sprintf("%T(%v):", resolver, r.ChanPoint) +func (r *contractResolverKit) initLogger(prefix string) { + logPrefix := fmt.Sprintf("ChannelArbitrator(%v): %s:", r.ChanPoint, + prefix) + r.log = log.WithPrefix(logPrefix) } diff --git a/contractcourt/htlc_success_resolver.go b/contractcourt/htlc_success_resolver.go index 06ebf4edc4a..363a5cc0444 100644 --- a/contractcourt/htlc_success_resolver.go +++ b/contractcourt/htlc_success_resolver.go @@ -2,6 +2,7 @@ package contractcourt import ( "encoding/binary" + "fmt" "io" "sync" @@ -81,27 +82,30 @@ func newSuccessResolver(res lnwallet.IncomingHtlcResolution, } h.initReport() + h.initLogger(fmt.Sprintf("%T(%v)", h, h.outpoint())) return h } -// ResolverKey returns an identifier which should be globally unique for this -// particular resolver within the chain the original contract resides within. -// -// NOTE: Part of the ContractResolver interface. -func (h *htlcSuccessResolver) ResolverKey() []byte { +// outpoint returns the outpoint of the HTLC output we're attempting to sweep. +func (h *htlcSuccessResolver) outpoint() wire.OutPoint { // The primary key for this resolver will be the outpoint of the HTLC // on the commitment transaction itself. If this is our commitment, // then the output can be found within the signed success tx, // otherwise, it's just the ClaimOutpoint. - var op wire.OutPoint if h.htlcResolution.SignedSuccessTx != nil { - op = h.htlcResolution.SignedSuccessTx.TxIn[0].PreviousOutPoint - } else { - op = h.htlcResolution.ClaimOutpoint + return h.htlcResolution.SignedSuccessTx.TxIn[0].PreviousOutPoint } - key := newResolverID(op) + return h.htlcResolution.ClaimOutpoint +} + +// ResolverKey returns an identifier which should be globally unique for this +// particular resolver within the chain the original contract resides within. +// +// NOTE: Part of the ContractResolver interface. +func (h *htlcSuccessResolver) ResolverKey() []byte { + key := newResolverID(h.outpoint()) return key[:] } @@ -679,6 +683,7 @@ func newSuccessResolverFromReader(r io.Reader, resCfg ResolverConfig) ( } h.initReport() + h.initLogger(fmt.Sprintf("%T(%v)", h, h.outpoint())) return h, nil } diff --git a/contractcourt/htlc_timeout_resolver.go b/contractcourt/htlc_timeout_resolver.go index 81d8e85d21c..ca456ec4c82 100644 --- a/contractcourt/htlc_timeout_resolver.go +++ b/contractcourt/htlc_timeout_resolver.go @@ -82,6 +82,7 @@ func newTimeoutResolver(res lnwallet.OutgoingHtlcResolution, } h.initReport() + h.initLogger(fmt.Sprintf("%T(%v)", h, h.outpoint())) return h } @@ -93,23 +94,25 @@ func (h *htlcTimeoutResolver) isTaproot() bool { ) } -// ResolverKey returns an identifier which should be globally unique for this -// particular resolver within the chain the original contract resides within. -// -// NOTE: Part of the ContractResolver interface. -func (h *htlcTimeoutResolver) ResolverKey() []byte { +// outpoint returns the outpoint of the HTLC output we're attempting to sweep. +func (h *htlcTimeoutResolver) outpoint() wire.OutPoint { // The primary key for this resolver will be the outpoint of the HTLC // on the commitment transaction itself. If this is our commitment, // then the output can be found within the signed timeout tx, // otherwise, it's just the ClaimOutpoint. - var op wire.OutPoint if h.htlcResolution.SignedTimeoutTx != nil { - op = h.htlcResolution.SignedTimeoutTx.TxIn[0].PreviousOutPoint - } else { - op = h.htlcResolution.ClaimOutpoint + return h.htlcResolution.SignedTimeoutTx.TxIn[0].PreviousOutPoint } - key := newResolverID(op) + return h.htlcResolution.ClaimOutpoint +} + +// ResolverKey returns an identifier which should be globally unique for this +// particular resolver within the chain the original contract resides within. +// +// NOTE: Part of the ContractResolver interface. +func (h *htlcTimeoutResolver) ResolverKey() []byte { + key := newResolverID(h.outpoint()) return key[:] } @@ -1038,6 +1041,7 @@ func newTimeoutResolverFromReader(r io.Reader, resCfg ResolverConfig) ( } h.initReport() + h.initLogger(fmt.Sprintf("%T(%v)", h, h.outpoint())) return h, nil } From 10e5a43e46c351f64c210dd268ec29772af77220 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 14 Nov 2024 02:28:56 +0800 Subject: [PATCH 33/92] contractcourt: add spend path helpers in timeout/success resolver This commit adds a few helper methods to decide how the htlc output should be spent. --- contractcourt/htlc_success_resolver.go | 43 +++++++++++++++++++------- contractcourt/htlc_timeout_resolver.go | 40 ++++++++++++++++++++---- 2 files changed, 65 insertions(+), 18 deletions(-) diff --git a/contractcourt/htlc_success_resolver.go b/contractcourt/htlc_success_resolver.go index 363a5cc0444..2dc61b18a06 100644 --- a/contractcourt/htlc_success_resolver.go +++ b/contractcourt/htlc_success_resolver.go @@ -127,7 +127,7 @@ func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) { // If we don't have a success transaction, then this means that this is // an output on the remote party's commitment transaction. - if h.htlcResolution.SignedSuccessTx == nil { + if h.isRemoteCommitOutput() { return h.resolveRemoteCommitOutput() } @@ -176,7 +176,7 @@ func (h *htlcSuccessResolver) broadcastSuccessTx() ( // and attach fees at will. We let the sweeper handle this job. We use // the checkpointed outputIncubating field to determine if we already // swept the HTLC output into the second level transaction. - if h.htlcResolution.SignDetails != nil { + if h.isZeroFeeOutput() { return h.broadcastReSignedSuccessTx() } @@ -236,12 +236,9 @@ func (h *htlcSuccessResolver) broadcastReSignedSuccessTx() (*wire.OutPoint, // We will have to let the sweeper re-sign the success tx and wait for // it to confirm, if we haven't already. - isTaproot := txscript.IsPayToTaproot( - h.htlcResolution.SweepSignDesc.Output.PkScript, - ) if !h.outputIncubating { var secondLevelInput input.HtlcSecondLevelAnchorInput - if isTaproot { + if h.isTaproot() { //nolint:ll secondLevelInput = input.MakeHtlcSecondLevelSuccessTaprootInput( h.htlcResolution.SignedSuccessTx, @@ -371,7 +368,7 @@ func (h *htlcSuccessResolver) broadcastReSignedSuccessTx() (*wire.OutPoint, // Let the sweeper sweep the second-level output now that the // CSV/CLTV locks have expired. var witType input.StandardWitnessType - if isTaproot { + if h.isTaproot() { witType = input.TaprootHtlcAcceptedSuccessSecondLevel } else { witType = input.HtlcAcceptedSuccessSecondLevel @@ -421,16 +418,12 @@ func (h *htlcSuccessResolver) broadcastReSignedSuccessTx() (*wire.OutPoint, func (h *htlcSuccessResolver) resolveRemoteCommitOutput() ( ContractResolver, error) { - isTaproot := txscript.IsPayToTaproot( - h.htlcResolution.SweepSignDesc.Output.PkScript, - ) - // Before we can craft out sweeping transaction, we need to // create an input which contains all the items required to add // this input to a sweeping transaction, and generate a // witness. var inp input.Input - if isTaproot { + if h.isTaproot() { inp = lnutils.Ptr(input.MakeTaprootHtlcSucceedInput( &h.htlcResolution.ClaimOutpoint, &h.htlcResolution.SweepSignDesc, @@ -712,3 +705,29 @@ func (h *htlcSuccessResolver) SupplementDeadline(_ fn.Option[int32]) { // A compile time assertion to ensure htlcSuccessResolver meets the // ContractResolver interface. var _ htlcContractResolver = (*htlcSuccessResolver)(nil) + +// isRemoteCommitOutput returns a bool to indicate whether the htlc output is +// on the remote commitment. +func (h *htlcSuccessResolver) isRemoteCommitOutput() bool { + // If we don't have a success transaction, then this means that this is + // an output on the remote party's commitment transaction. + return h.htlcResolution.SignedSuccessTx == nil +} + +// isZeroFeeOutput returns a boolean indicating whether the htlc output is from +// a anchor-enabled channel, which uses the sighash SINGLE|ANYONECANPAY. +func (h *htlcSuccessResolver) isZeroFeeOutput() bool { + // If we have non-nil SignDetails, this means it has a 2nd level HTLC + // transaction that is signed using sighash SINGLE|ANYONECANPAY (the + // case for anchor type channels). In this case we can re-sign it and + // attach fees at will. + return h.htlcResolution.SignedSuccessTx != nil && + h.htlcResolution.SignDetails != nil +} + +// isTaproot returns true if the resolver is for a taproot output. +func (h *htlcSuccessResolver) isTaproot() bool { + return txscript.IsPayToTaproot( + h.htlcResolution.SweepSignDesc.Output.PkScript, + ) +} diff --git a/contractcourt/htlc_timeout_resolver.go b/contractcourt/htlc_timeout_resolver.go index ca456ec4c82..698b4ae21ec 100644 --- a/contractcourt/htlc_timeout_resolver.go +++ b/contractcourt/htlc_timeout_resolver.go @@ -634,7 +634,7 @@ func (h *htlcTimeoutResolver) spendHtlcOutput() ( // HTLC transaction that is signed using sighash SINGLE|ANYONECANPAY // (the case for anchor type channels). In this case we can re-sign it // and attach fees at will. We let the sweeper handle this job. - case h.htlcResolution.SignDetails != nil && !h.outputIncubating: + case h.isZeroFeeOutput() && !h.outputIncubating: if err := h.sweepSecondLevelTx(); err != nil { log.Errorf("Sending timeout tx to sweeper: %v", err) @@ -643,7 +643,7 @@ func (h *htlcTimeoutResolver) spendHtlcOutput() ( // If this is a remote commitment there's no second level timeout txn, // and we can just send this directly to the sweeper. - case h.htlcResolution.SignedTimeoutTx == nil && !h.outputIncubating: + case h.isRemoteCommitOutput() && !h.outputIncubating: if err := h.sweepDirectHtlcOutput(); err != nil { log.Errorf("Sending direct spend to sweeper: %v", err) @@ -653,7 +653,7 @@ func (h *htlcTimeoutResolver) spendHtlcOutput() ( // If we have a SignedTimeoutTx but no SignDetails, this is a local // commitment for a non-anchor channel, so we'll send it to the utxo // nursery. - case h.htlcResolution.SignDetails == nil && !h.outputIncubating: + case h.isLegacyOutput() && !h.outputIncubating: if err := h.sendSecondLevelTxLegacy(); err != nil { log.Errorf("Sending timeout tx to nursery: %v", err) @@ -769,7 +769,7 @@ func (h *htlcTimeoutResolver) handleCommitSpend( // If the sweeper is handling the second level transaction, wait for // the CSV and possible CLTV lock to expire, before sweeping the output // on the second-level. - case h.htlcResolution.SignDetails != nil: + case h.isZeroFeeOutput(): waitHeight := h.deriveWaitHeight( h.htlcResolution.CsvDelay, commitSpend, ) @@ -851,7 +851,7 @@ func (h *htlcTimeoutResolver) handleCommitSpend( // Finally, if this was an output on our commitment transaction, we'll // wait for the second-level HTLC output to be spent, and for that // transaction itself to confirm. - case h.htlcResolution.SignedTimeoutTx != nil: + case !h.isRemoteCommitOutput(): log.Infof("%T(%v): waiting for nursery/sweeper to spend CSV "+ "delayed output", h, claimOutpoint) @@ -1232,7 +1232,7 @@ func (h *htlcTimeoutResolver) consumeSpendEvents(resultChan chan *spendResult, // continue the loop. hasPreimage := isPreimageSpend( h.isTaproot(), spendDetail, - h.htlcResolution.SignedTimeoutTx != nil, + !h.isRemoteCommitOutput(), ) if !hasPreimage { log.Debugf("HTLC output %s spent doesn't "+ @@ -1260,3 +1260,31 @@ func (h *htlcTimeoutResolver) consumeSpendEvents(resultChan chan *spendResult, } } } + +// isRemoteCommitOutput returns a bool to indicate whether the htlc output is +// on the remote commitment. +func (h *htlcTimeoutResolver) isRemoteCommitOutput() bool { + // If we don't have a timeout transaction, then this means that this is + // an output on the remote party's commitment transaction. + return h.htlcResolution.SignedTimeoutTx == nil +} + +// isZeroFeeOutput returns a boolean indicating whether the htlc output is from +// a anchor-enabled channel, which uses the sighash SINGLE|ANYONECANPAY. +func (h *htlcTimeoutResolver) isZeroFeeOutput() bool { + // If we have non-nil SignDetails, this means it has a 2nd level HTLC + // transaction that is signed using sighash SINGLE|ANYONECANPAY (the + // case for anchor type channels). In this case we can re-sign it and + // attach fees at will. + return h.htlcResolution.SignedTimeoutTx != nil && + h.htlcResolution.SignDetails != nil +} + +// isLegacyOutput returns a boolean indicating whether the htlc output is from +// a non-anchor-enabled channel. +func (h *htlcTimeoutResolver) isLegacyOutput() bool { + // If we have a SignedTimeoutTx but no SignDetails, this is a local + // commitment for a non-anchor channel. + return h.htlcResolution.SignedTimeoutTx != nil && + h.htlcResolution.SignDetails == nil +} From fb499bc4ccba8c2dd8b73e916e901b535beeeb2f Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 14 Nov 2024 21:59:16 +0800 Subject: [PATCH 34/92] contractcourt: add sweep senders in `htlcSuccessResolver` This commit is a pure refactor in which moves the sweep handling logic into the new methods. --- contractcourt/htlc_success_resolver.go | 381 +++++++++++++------------ 1 file changed, 199 insertions(+), 182 deletions(-) diff --git a/contractcourt/htlc_success_resolver.go b/contractcourt/htlc_success_resolver.go index 2dc61b18a06..3531d1fc92e 100644 --- a/contractcourt/htlc_success_resolver.go +++ b/contractcourt/htlc_success_resolver.go @@ -237,55 +237,7 @@ func (h *htlcSuccessResolver) broadcastReSignedSuccessTx() (*wire.OutPoint, // We will have to let the sweeper re-sign the success tx and wait for // it to confirm, if we haven't already. if !h.outputIncubating { - var secondLevelInput input.HtlcSecondLevelAnchorInput - if h.isTaproot() { - //nolint:ll - secondLevelInput = input.MakeHtlcSecondLevelSuccessTaprootInput( - h.htlcResolution.SignedSuccessTx, - h.htlcResolution.SignDetails, h.htlcResolution.Preimage, - h.broadcastHeight, - input.WithResolutionBlob( - h.htlcResolution.ResolutionBlob, - ), - ) - } else { - //nolint:ll - secondLevelInput = input.MakeHtlcSecondLevelSuccessAnchorInput( - h.htlcResolution.SignedSuccessTx, - h.htlcResolution.SignDetails, h.htlcResolution.Preimage, - h.broadcastHeight, - ) - } - - // Calculate the budget for this sweep. - value := btcutil.Amount( - secondLevelInput.SignDesc().Output.Value, - ) - budget := calculateBudget( - value, h.Budget.DeadlineHTLCRatio, - h.Budget.DeadlineHTLC, - ) - - // The deadline would be the CLTV in this HTLC output. If we - // are the initiator of this force close, with the default - // `IncomingBroadcastDelta`, it means we have 10 blocks left - // when going onchain. Given we need to mine one block to - // confirm the force close tx, and one more block to trigger - // the sweep, we have 8 blocks left to sweep the HTLC. - deadline := fn.Some(int32(h.htlc.RefundTimeout)) - - log.Infof("%T(%x): offering second-level HTLC success tx to "+ - "sweeper with deadline=%v, budget=%v", h, - h.htlc.RHash[:], h.htlc.RefundTimeout, budget) - - // We'll now offer the second-level transaction to the sweeper. - _, err := h.Sweeper.SweepInput( - &secondLevelInput, - sweep.Params{ - Budget: budget, - DeadlineHeight: deadline, - }, - ) + err := h.sweepSuccessTx() if err != nil { return nil, err } @@ -316,99 +268,18 @@ func (h *htlcSuccessResolver) broadcastReSignedSuccessTx() (*wire.OutPoint, "confirmed!", h, h.htlc.RHash[:]) } - // If we ended up here after a restart, we must again get the - // spend notification. - if commitSpend == nil { - var err error - commitSpend, err = waitForSpend( - &h.htlcResolution.SignedSuccessTx.TxIn[0].PreviousOutPoint, - h.htlcResolution.SignDetails.SignDesc.Output.PkScript, - h.broadcastHeight, h.Notifier, h.quit, - ) - if err != nil { - return nil, err - } - } - - // 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 - // to wait for longer. - waitHeight := h.deriveWaitHeight( - h.htlcResolution.CsvDelay, commitSpend, - ) - - // Now that the sweeper has broadcasted the second-level transaction, - // it has confirmed, and we have checkpointed our state, we'll sweep - // the second level output. We report the resolver has moved the next - // stage. - h.reportLock.Lock() - h.currentReport.Stage = 2 - h.currentReport.MaturityHeight = waitHeight - h.reportLock.Unlock() - - if h.hasCLTV() { - log.Infof("%T(%x): waiting for CSV and CLTV lock to "+ - "expire at height %v", h, h.htlc.RHash[:], - waitHeight) - } else { - log.Infof("%T(%x): waiting for CSV lock to expire at "+ - "height %v", h, h.htlc.RHash[:], waitHeight) + err := h.sweepSuccessTxOutput() + if err != nil { + return nil, 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. + // Will return this outpoint, when this is spent the resolver is fully + // resolved. 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 - if h.isTaproot() { - witType = input.TaprootHtlcAcceptedSuccessSecondLevel - } else { - witType = input.HtlcAcceptedSuccessSecondLevel - } - inp := h.makeSweepInput( - op, witType, - input.LeaseHtlcAcceptedSuccessSecondLevel, - &h.htlcResolution.SweepSignDesc, - h.htlcResolution.CsvDelay, uint32(commitSpend.SpendingHeight), - h.htlc.RHash, h.htlcResolution.ResolutionBlob, - ) - - // Calculate the budget for this sweep. - budget := calculateBudget( - btcutil.Amount(inp.SignDesc().Output.Value), - h.Budget.NoDeadlineHTLCRatio, - h.Budget.NoDeadlineHTLC, - ) - - log.Infof("%T(%x): offering second-level success tx output to sweeper "+ - "with no deadline and budget=%v at height=%v", h, - h.htlc.RHash[:], budget, waitHeight) - - // TODO(roasbeef): need to update above for leased types - _, err := h.Sweeper.SweepInput( - inp, - sweep.Params{ - Budget: budget, - - // For second level success tx, there's no rush to get - // it confirmed, so we use a nil deadline. - DeadlineHeight: fn.None[int32](), - }, - ) - if err != nil { - return nil, err - } - - // Will return this outpoint, when this is spent the resolver is fully - // resolved. return op, nil } @@ -418,53 +289,7 @@ func (h *htlcSuccessResolver) broadcastReSignedSuccessTx() (*wire.OutPoint, func (h *htlcSuccessResolver) resolveRemoteCommitOutput() ( ContractResolver, error) { - // Before we can craft out sweeping transaction, we need to - // create an input which contains all the items required to add - // this input to a sweeping transaction, and generate a - // witness. - var inp input.Input - if h.isTaproot() { - inp = lnutils.Ptr(input.MakeTaprootHtlcSucceedInput( - &h.htlcResolution.ClaimOutpoint, - &h.htlcResolution.SweepSignDesc, - h.htlcResolution.Preimage[:], - h.broadcastHeight, - h.htlcResolution.CsvDelay, - input.WithResolutionBlob( - h.htlcResolution.ResolutionBlob, - ), - )) - } else { - inp = lnutils.Ptr(input.MakeHtlcSucceedInput( - &h.htlcResolution.ClaimOutpoint, - &h.htlcResolution.SweepSignDesc, - h.htlcResolution.Preimage[:], - h.broadcastHeight, - h.htlcResolution.CsvDelay, - )) - } - - // Calculate the budget for this sweep. - budget := calculateBudget( - btcutil.Amount(inp.SignDesc().Output.Value), - h.Budget.DeadlineHTLCRatio, - h.Budget.DeadlineHTLC, - ) - - deadline := fn.Some(int32(h.htlc.RefundTimeout)) - - log.Infof("%T(%x): offering direct-preimage HTLC output to sweeper "+ - "with deadline=%v, budget=%v", h, h.htlc.RHash[:], - h.htlc.RefundTimeout, budget) - - // We'll now offer the direct preimage HTLC to the sweeper. - _, err := h.Sweeper.SweepInput( - inp, - sweep.Params{ - Budget: budget, - DeadlineHeight: deadline, - }, - ) + err := h.sweepRemoteCommitOutput() if err != nil { return nil, err } @@ -731,3 +556,195 @@ func (h *htlcSuccessResolver) isTaproot() bool { h.htlcResolution.SweepSignDesc.Output.PkScript, ) } + +// sweepRemoteCommitOutput creates a sweep request to sweep the HTLC output on +// the remote commitment via the direct preimage-spend. +func (h *htlcSuccessResolver) sweepRemoteCommitOutput() error { + // Before we can craft out sweeping transaction, we need to create an + // input which contains all the items required to add this input to a + // sweeping transaction, and generate a witness. + var inp input.Input + + if h.isTaproot() { + inp = lnutils.Ptr(input.MakeTaprootHtlcSucceedInput( + &h.htlcResolution.ClaimOutpoint, + &h.htlcResolution.SweepSignDesc, + h.htlcResolution.Preimage[:], + h.broadcastHeight, + h.htlcResolution.CsvDelay, + input.WithResolutionBlob( + h.htlcResolution.ResolutionBlob, + ), + )) + } else { + inp = lnutils.Ptr(input.MakeHtlcSucceedInput( + &h.htlcResolution.ClaimOutpoint, + &h.htlcResolution.SweepSignDesc, + h.htlcResolution.Preimage[:], + h.broadcastHeight, + h.htlcResolution.CsvDelay, + )) + } + + // Calculate the budget for this sweep. + budget := calculateBudget( + btcutil.Amount(inp.SignDesc().Output.Value), + h.Budget.DeadlineHTLCRatio, + h.Budget.DeadlineHTLC, + ) + + deadline := fn.Some(int32(h.htlc.RefundTimeout)) + + log.Infof("%T(%x): offering direct-preimage HTLC output to sweeper "+ + "with deadline=%v, budget=%v", h, h.htlc.RHash[:], + h.htlc.RefundTimeout, budget) + + // We'll now offer the direct preimage HTLC to the sweeper. + _, err := h.Sweeper.SweepInput( + inp, + sweep.Params{ + Budget: budget, + DeadlineHeight: deadline, + }, + ) + + return err +} + +// sweepSuccessTx attempts to sweep the second level success tx. +func (h *htlcSuccessResolver) sweepSuccessTx() error { + var secondLevelInput input.HtlcSecondLevelAnchorInput + if h.isTaproot() { + secondLevelInput = input.MakeHtlcSecondLevelSuccessTaprootInput( + h.htlcResolution.SignedSuccessTx, + h.htlcResolution.SignDetails, h.htlcResolution.Preimage, + h.broadcastHeight, input.WithResolutionBlob( + h.htlcResolution.ResolutionBlob, + ), + ) + } else { + secondLevelInput = input.MakeHtlcSecondLevelSuccessAnchorInput( + h.htlcResolution.SignedSuccessTx, + h.htlcResolution.SignDetails, h.htlcResolution.Preimage, + h.broadcastHeight, + ) + } + + // Calculate the budget for this sweep. + value := btcutil.Amount(secondLevelInput.SignDesc().Output.Value) + budget := calculateBudget( + value, h.Budget.DeadlineHTLCRatio, h.Budget.DeadlineHTLC, + ) + + // The deadline would be the CLTV in this HTLC output. If we are the + // initiator of this force close, with the default + // `IncomingBroadcastDelta`, it means we have 10 blocks left when going + // onchain. + deadline := fn.Some(int32(h.htlc.RefundTimeout)) + + h.log.Infof("offering second-level HTLC success tx to sweeper with "+ + "deadline=%v, budget=%v", h.htlc.RefundTimeout, budget) + + // We'll now offer the second-level transaction to the sweeper. + _, err := h.Sweeper.SweepInput( + &secondLevelInput, + sweep.Params{ + Budget: budget, + DeadlineHeight: deadline, + }, + ) + + return err +} + +// sweepSuccessTxOutput attempts to sweep the output of the second level +// success tx. +func (h *htlcSuccessResolver) sweepSuccessTxOutput() error { + h.log.Debugf("sweeping output %v from 2nd-level HTLC success tx", + h.htlcResolution.ClaimOutpoint) + + // This should be non-blocking as we will only attempt to sweep the + // output when the second level tx has already been confirmed. In other + // words, waitForSpend will return immediately. + commitSpend, err := waitForSpend( + &h.htlcResolution.SignedSuccessTx.TxIn[0].PreviousOutPoint, + h.htlcResolution.SignDetails.SignDesc.Output.PkScript, + h.broadcastHeight, h.Notifier, h.quit, + ) + if err != nil { + return err + } + + // 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 + // to wait for longer. + waitHeight := h.deriveWaitHeight(h.htlcResolution.CsvDelay, commitSpend) + + // Now that the sweeper has broadcasted the second-level transaction, + // it has confirmed, and we have checkpointed our state, we'll sweep + // the second level output. We report the resolver has moved the next + // stage. + h.reportLock.Lock() + h.currentReport.Stage = 2 + h.currentReport.MaturityHeight = waitHeight + h.reportLock.Unlock() + + if h.hasCLTV() { + log.Infof("%T(%x): waiting for CSV and CLTV lock to expire at "+ + "height %v", h, h.htlc.RHash[:], waitHeight) + } else { + log.Infof("%T(%x): waiting for CSV lock to expire at height %v", + 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 + if h.isTaproot() { + witType = input.TaprootHtlcAcceptedSuccessSecondLevel + } else { + witType = input.HtlcAcceptedSuccessSecondLevel + } + inp := h.makeSweepInput( + op, witType, + input.LeaseHtlcAcceptedSuccessSecondLevel, + &h.htlcResolution.SweepSignDesc, + h.htlcResolution.CsvDelay, uint32(commitSpend.SpendingHeight), + h.htlc.RHash, h.htlcResolution.ResolutionBlob, + ) + + // Calculate the budget for this sweep. + budget := calculateBudget( + btcutil.Amount(inp.SignDesc().Output.Value), + h.Budget.NoDeadlineHTLCRatio, + h.Budget.NoDeadlineHTLC, + ) + + log.Infof("%T(%x): offering second-level success tx output to sweeper "+ + "with no deadline and budget=%v at height=%v", h, + h.htlc.RHash[:], budget, waitHeight) + + // TODO(yy): use the result chan returned from SweepInput. + _, err = h.Sweeper.SweepInput( + inp, + sweep.Params{ + Budget: budget, + + // For second level success tx, there's no rush to get + // it confirmed, so we use a nil deadline. + DeadlineHeight: fn.None[int32](), + }, + ) + + return err +} From c92d7f0fd0cc18cbc1e58d1979253a5f2e26baa9 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 14 Nov 2024 21:59:55 +0800 Subject: [PATCH 35/92] contractcourt: add resolver handlers in `htlcSuccessResolver` This commit refactors the `Resolve` method by adding two resolver handlers to handle waiting for spending confirmations. --- contractcourt/htlc_success_resolver.go | 229 ++++++++++++++++--------- contractcourt/htlc_timeout_resolver.go | 8 +- 2 files changed, 149 insertions(+), 88 deletions(-) diff --git a/contractcourt/htlc_success_resolver.go b/contractcourt/htlc_success_resolver.go index 3531d1fc92e..b436ccda89b 100644 --- a/contractcourt/htlc_success_resolver.go +++ b/contractcourt/htlc_success_resolver.go @@ -140,27 +140,7 @@ func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) { // To wrap this up, we'll wait until the second-level transaction has // been spent, then fully resolve the contract. - log.Infof("%T(%x): waiting for second-level HTLC output to be spent "+ - "after csv_delay=%v", h, h.htlc.RHash[:], h.htlcResolution.CsvDelay) - - spend, err := waitForSpend( - secondLevelOutpoint, - h.htlcResolution.SweepSignDesc.Output.PkScript, - h.broadcastHeight, h.Notifier, h.quit, - ) - if err != nil { - return nil, err - } - - h.reportLock.Lock() - h.currentReport.RecoveredBalance = h.currentReport.LimboBalance - h.currentReport.LimboBalance = 0 - h.reportLock.Unlock() - - h.resolved = true - return nil, h.checkpointClaim( - spend.SpenderTxHash, channeldb.ResolverOutcomeClaimed, - ) + return nil, h.resolveSuccessTxOutput(*secondLevelOutpoint) } // broadcastSuccessTx handles an HTLC output on our local commitment by @@ -187,40 +167,11 @@ func (h *htlcSuccessResolver) broadcastSuccessTx() ( // We'll now broadcast the second layer transaction so we can kick off // the claiming process. - // - // TODO(roasbeef): after changing sighashes send to tx bundler - label := labels.MakeLabel( - labels.LabelTypeChannelClose, &h.ShortChanID, - ) - err := h.PublishTx(h.htlcResolution.SignedSuccessTx, label) + err := h.resolveLegacySuccessTx() if err != nil { return nil, err } - // Otherwise, this is an output on our commitment transaction. In this - // case, we'll send it to the incubator, but only if we haven't already - // done so. - if !h.outputIncubating { - log.Infof("%T(%x): incubating incoming htlc output", - h, h.htlc.RHash[:]) - - err := h.IncubateOutputs( - h.ChanPoint, fn.None[lnwallet.OutgoingHtlcResolution](), - fn.Some(h.htlcResolution), - h.broadcastHeight, fn.Some(int32(h.htlc.RefundTimeout)), - ) - if err != nil { - return nil, err - } - - h.outputIncubating = true - - if err := h.Checkpoint(h); err != nil { - log.Errorf("unable to Checkpoint: %v", err) - return nil, err - } - } - return &h.htlcResolution.ClaimOutpoint, nil } @@ -242,33 +193,25 @@ func (h *htlcSuccessResolver) broadcastReSignedSuccessTx() (*wire.OutPoint, return nil, err } - log.Infof("%T(%x): waiting for second-level HTLC success "+ - "transaction to confirm", h, h.htlc.RHash[:]) - - // Wait for the second level transaction to confirm. - commitSpend, err = waitForSpend( - &h.htlcResolution.SignedSuccessTx.TxIn[0].PreviousOutPoint, - h.htlcResolution.SignDetails.SignDesc.Output.PkScript, - h.broadcastHeight, h.Notifier, h.quit, - ) + err = h.resolveSuccessTx() if err != nil { return nil, err } + } - // Now that the second-level transaction has confirmed, we - // checkpoint the state so we'll go to the next stage in case - // of restarts. - h.outputIncubating = true - if err := h.Checkpoint(h); err != nil { - log.Errorf("unable to Checkpoint: %v", err) - return nil, err - } - - log.Infof("%T(%x): second-level HTLC success transaction "+ - "confirmed!", h, h.htlc.RHash[:]) + // This should be non-blocking as we will only attempt to sweep the + // output when the second level tx has already been confirmed. In other + // words, waitForSpend will return immediately. + commitSpend, err := waitForSpend( + &h.htlcResolution.SignedSuccessTx.TxIn[0].PreviousOutPoint, + h.htlcResolution.SignDetails.SignDesc.Output.PkScript, + h.broadcastHeight, h.Notifier, h.quit, + ) + if err != nil { + return nil, err } - err := h.sweepSuccessTxOutput() + err = h.sweepSuccessTxOutput() if err != nil { return nil, err } @@ -304,23 +247,14 @@ func (h *htlcSuccessResolver) resolveRemoteCommitOutput() ( return nil, err } - // Once the transaction has received a sufficient number of - // confirmations, we'll mark ourselves as fully resolved and exit. - h.resolved = true - // Checkpoint the resolver, and write the outcome to disk. - return nil, h.checkpointClaim( - sweepTxDetails.SpenderTxHash, - channeldb.ResolverOutcomeClaimed, - ) + return nil, h.checkpointClaim(sweepTxDetails.SpenderTxHash) } // checkpointClaim checkpoints the success resolver with the reports it needs. // If this htlc was claimed two stages, it will write reports for both stages, // otherwise it will just write for the single htlc claim. -func (h *htlcSuccessResolver) checkpointClaim(spendTx *chainhash.Hash, - outcome channeldb.ResolverOutcome) error { - +func (h *htlcSuccessResolver) checkpointClaim(spendTx *chainhash.Hash) error { // Mark the htlc as final settled. err := h.ChainArbitratorConfig.PutFinalHtlcOutcome( h.ChannelArbitratorConfig.ShortChanID, h.htlc.HtlcIndex, true, @@ -348,7 +282,7 @@ func (h *htlcSuccessResolver) checkpointClaim(spendTx *chainhash.Hash, OutPoint: h.htlcResolution.ClaimOutpoint, Amount: amt, ResolverType: channeldb.ResolverTypeIncomingHtlc, - ResolverOutcome: outcome, + ResolverOutcome: channeldb.ResolverOutcomeClaimed, SpendTxID: spendTx, }, } @@ -373,6 +307,7 @@ func (h *htlcSuccessResolver) checkpointClaim(spendTx *chainhash.Hash, } // Finally, we checkpoint the resolver with our report(s). + h.resolved = true return h.Checkpoint(h, reports...) } @@ -748,3 +683,129 @@ func (h *htlcSuccessResolver) sweepSuccessTxOutput() error { return err } + +// resolveLegacySuccessTx handles an HTLC output from a pre-anchor type channel +// by broadcasting the second-level success transaction. +func (h *htlcSuccessResolver) resolveLegacySuccessTx() error { + // Otherwise we'll publish the second-level transaction directly and + // offer the resolution to the nursery to handle. + h.log.Infof("broadcasting legacy second-level success tx: %v", + h.htlcResolution.SignedSuccessTx.TxHash()) + + // We'll now broadcast the second layer transaction so we can kick off + // the claiming process. + // + // TODO(yy): offer it to the sweeper instead. + label := labels.MakeLabel( + labels.LabelTypeChannelClose, &h.ShortChanID, + ) + err := h.PublishTx(h.htlcResolution.SignedSuccessTx, label) + if err != nil { + return err + } + + // Fast-forward to resolve the output from the success tx if the it has + // already been sent to the UtxoNursery. + if h.outputIncubating { + return h.resolveSuccessTxOutput(h.htlcResolution.ClaimOutpoint) + } + + h.log.Infof("incubating incoming htlc output") + + // Send the output to the incubator. + err = h.IncubateOutputs( + h.ChanPoint, fn.None[lnwallet.OutgoingHtlcResolution](), + fn.Some(h.htlcResolution), + h.broadcastHeight, fn.Some(int32(h.htlc.RefundTimeout)), + ) + if err != nil { + return err + } + + // Mark the output as incubating and checkpoint it. + h.outputIncubating = true + if err := h.Checkpoint(h); err != nil { + return err + } + + // Move to resolve the output. + return h.resolveSuccessTxOutput(h.htlcResolution.ClaimOutpoint) +} + +// resolveSuccessTx waits for the sweeping tx of the second-level success tx to +// confirm and offers the output from the success tx to the sweeper. +func (h *htlcSuccessResolver) resolveSuccessTx() error { + h.log.Infof("waiting for 2nd-level HTLC success transaction to confirm") + + // Create aliases to make the code more readable. + outpoint := h.htlcResolution.SignedSuccessTx.TxIn[0].PreviousOutPoint + pkScript := h.htlcResolution.SignDetails.SignDesc.Output.PkScript + + // Wait for the second level transaction to confirm. + commitSpend, err := waitForSpend( + &outpoint, pkScript, h.broadcastHeight, h.Notifier, h.quit, + ) + 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, + } + + // If the 2nd-stage sweeping has already been started, we can + // fast-forward to start the resolving process for the stage two + // output. + if h.outputIncubating { + return h.resolveSuccessTxOutput(op) + } + + // Now that the second-level transaction has confirmed, we checkpoint + // the state so we'll go to the next stage in case of restarts. + h.outputIncubating = true + if err := h.Checkpoint(h); err != nil { + log.Errorf("unable to Checkpoint: %v", err) + return err + } + + h.log.Infof("2nd-level HTLC success tx=%v confirmed", + commitSpend.SpenderTxHash) + + // Send the sweep request for the output from the success tx. + if err := h.sweepSuccessTxOutput(); err != nil { + return err + } + + return h.resolveSuccessTxOutput(op) +} + +// resolveSuccessTxOutput waits for the spend of the output from the 2nd-level +// success tx. +func (h *htlcSuccessResolver) resolveSuccessTxOutput(op wire.OutPoint) error { + // To wrap this up, we'll wait until the second-level transaction has + // been spent, then fully resolve the contract. + log.Infof("%T(%x): waiting for second-level HTLC output to be spent "+ + "after csv_delay=%v", h, h.htlc.RHash[:], + h.htlcResolution.CsvDelay) + + spend, err := waitForSpend( + &op, h.htlcResolution.SweepSignDesc.Output.PkScript, + h.broadcastHeight, h.Notifier, h.quit, + ) + if err != nil { + return err + } + + h.reportLock.Lock() + h.currentReport.RecoveredBalance = h.currentReport.LimboBalance + h.currentReport.LimboBalance = 0 + h.reportLock.Unlock() + + return h.checkpointClaim(spend.SpenderTxHash) +} diff --git a/contractcourt/htlc_timeout_resolver.go b/contractcourt/htlc_timeout_resolver.go index 698b4ae21ec..24167e70a46 100644 --- a/contractcourt/htlc_timeout_resolver.go +++ b/contractcourt/htlc_timeout_resolver.go @@ -548,9 +548,9 @@ func (h *htlcTimeoutResolver) sweepSecondLevelTx() error { return err } -// sendSecondLevelTxLegacy sends a second level timeout transaction to the utxo -// nursery. This transaction uses the legacy SIGHASH_ALL flag. -func (h *htlcTimeoutResolver) sendSecondLevelTxLegacy() error { +// resolveSecondLevelTxLegacy sends a second level timeout transaction to the +// utxo nursery. This transaction uses the legacy SIGHASH_ALL flag. +func (h *htlcTimeoutResolver) resolveSecondLevelTxLegacy() error { log.Debugf("%T(%v): incubating htlc output", h, h.htlcResolution.ClaimOutpoint) @@ -654,7 +654,7 @@ func (h *htlcTimeoutResolver) spendHtlcOutput() ( // commitment for a non-anchor channel, so we'll send it to the utxo // nursery. case h.isLegacyOutput() && !h.outputIncubating: - if err := h.sendSecondLevelTxLegacy(); err != nil { + if err := h.resolveSecondLevelTxLegacy(); err != nil { log.Errorf("Sending timeout tx to nursery: %v", err) return nil, err From cb18940e75fa92be1cef09dd1b70cfb78b5e853b Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 14 Nov 2024 22:07:32 +0800 Subject: [PATCH 36/92] contractcourt: remove redundant return value in `claimCleanUp` --- contractcourt/htlc_outgoing_contest_resolver.go | 4 ++-- contractcourt/htlc_timeout_resolver.go | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/contractcourt/htlc_outgoing_contest_resolver.go b/contractcourt/htlc_outgoing_contest_resolver.go index ec32ff7f172..7adce5a6893 100644 --- a/contractcourt/htlc_outgoing_contest_resolver.go +++ b/contractcourt/htlc_outgoing_contest_resolver.go @@ -87,7 +87,7 @@ func (h *htlcOutgoingContestResolver) Resolve() (ContractResolver, error) { } // TODO(roasbeef): Checkpoint? - return h.claimCleanUp(commitSpend) + return nil, h.claimCleanUp(commitSpend) // If it hasn't, then we'll watch for both the expiration, and the // sweeping out this output. @@ -144,7 +144,7 @@ func (h *htlcOutgoingContestResolver) Resolve() (ContractResolver, error) { // party is by revealing the preimage. So we'll perform // our duties to clean up the contract once it has been // claimed. - return h.claimCleanUp(commitSpend) + return nil, h.claimCleanUp(commitSpend) case <-h.quit: return nil, fmt.Errorf("resolver canceled") diff --git a/contractcourt/htlc_timeout_resolver.go b/contractcourt/htlc_timeout_resolver.go index 24167e70a46..b0a57352b8f 100644 --- a/contractcourt/htlc_timeout_resolver.go +++ b/contractcourt/htlc_timeout_resolver.go @@ -160,7 +160,7 @@ const ( // by the remote party. It'll extract the preimage, add it to the global cache, // and finally send the appropriate clean up message. func (h *htlcTimeoutResolver) claimCleanUp( - commitSpend *chainntnfs.SpendDetail) (ContractResolver, error) { + commitSpend *chainntnfs.SpendDetail) error { // Depending on if this is our commitment or not, then we'll be looking // for a different witness pattern. @@ -195,7 +195,7 @@ func (h *htlcTimeoutResolver) claimCleanUp( // element, then we're actually on the losing side of a breach // attempt... case h.isTaproot() && len(spendingInput.Witness) == 1: - return nil, fmt.Errorf("breach attempt failed") + return fmt.Errorf("breach attempt failed") // Otherwise, they'll be spending directly from our commitment output. // In which case the witness stack looks like: @@ -212,8 +212,8 @@ func (h *htlcTimeoutResolver) claimCleanUp( preimage, err := lntypes.MakePreimage(preimageBytes) if err != nil { - return nil, fmt.Errorf("unable to create pre-image from "+ - "witness: %v", err) + return fmt.Errorf("unable to create pre-image from witness: %w", + err) } log.Infof("%T(%v): extracting preimage=%v from on-chain "+ @@ -235,7 +235,7 @@ func (h *htlcTimeoutResolver) claimCleanUp( HtlcIndex: h.htlc.HtlcIndex, PreImage: &pre, }); err != nil { - return nil, err + return err } h.resolved = true @@ -250,7 +250,7 @@ func (h *htlcTimeoutResolver) claimCleanUp( SpendTxID: commitSpend.SpenderTxHash, } - return nil, h.Checkpoint(h, report) + return h.Checkpoint(h, report) } // chainDetailsToWatch returns the output and script which we use to watch for @@ -448,7 +448,7 @@ func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) { "witness cache", h, h.htlc.RHash[:], h.htlcResolution.ClaimOutpoint) - return h.claimCleanUp(commitSpend) + return nil, h.claimCleanUp(commitSpend) } // At this point, the second-level transaction is sufficiently From bfc95b8b2c116115acd977f4b97d6e8c602afbe8 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 14 Nov 2024 22:08:00 +0800 Subject: [PATCH 37/92] contractcourt: add sweep senders in `htlcTimeoutResolver` This commit adds new methods to handle making sweep requests based on the spending path used by the outgoing htlc output. --- contractcourt/htlc_timeout_resolver.go | 251 ++++++++++++++----------- 1 file changed, 140 insertions(+), 111 deletions(-) diff --git a/contractcourt/htlc_timeout_resolver.go b/contractcourt/htlc_timeout_resolver.go index b0a57352b8f..d17059364cf 100644 --- a/contractcourt/htlc_timeout_resolver.go +++ b/contractcourt/htlc_timeout_resolver.go @@ -476,13 +476,9 @@ func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) { return h.handleCommitSpend(commitSpend) } -// sweepSecondLevelTx sends a second level timeout transaction to the sweeper. +// sweepTimeoutTx sends a second level timeout transaction to the sweeper. // This transaction uses the SINLGE|ANYONECANPAY flag. -func (h *htlcTimeoutResolver) sweepSecondLevelTx() error { - log.Infof("%T(%x): offering second-layer timeout tx to sweeper: %v", - h, h.htlc.RHash[:], - spew.Sdump(h.htlcResolution.SignedTimeoutTx)) - +func (h *htlcTimeoutResolver) sweepTimeoutTx() error { var inp input.Input if h.isTaproot() { inp = lnutils.Ptr(input.MakeHtlcSecondLevelTimeoutTaprootInput( @@ -513,27 +509,12 @@ func (h *htlcTimeoutResolver) sweepSecondLevelTx() error { btcutil.Amount(inp.SignDesc().Output.Value), 2, 0, ) + h.log.Infof("offering 2nd-level HTLC timeout tx to sweeper "+ + "with deadline=%v, budget=%v", h.incomingHTLCExpiryHeight, + budget) + // For an outgoing HTLC, it must be swept before the RefundTimeout of // its incoming HTLC is reached. - // - // TODO(yy): we may end up mixing inputs with different time locks. - // Suppose we have two outgoing HTLCs, - // - HTLC1: nLocktime is 800000, CLTV delta is 80. - // - HTLC2: nLocktime is 800001, CLTV delta is 79. - // This means they would both have an incoming HTLC that expires at - // 800080, hence they share the same deadline but different locktimes. - // However, with current design, when we are at block 800000, HTLC1 is - // offered to the sweeper. When block 800001 is reached, HTLC1's - // sweeping process is already started, while HTLC2 is being offered to - // the sweeper, so they won't be mixed. This can become an issue tho, - // if we decide to sweep per X blocks. Or the contractcourt sees the - // block first while the sweeper is only aware of the last block. To - // properly fix it, we need `blockbeat` to make sure subsystems are in - // sync. - log.Infof("%T(%x): offering second-level HTLC timeout tx to sweeper "+ - "with deadline=%v, budget=%v", h, h.htlc.RHash[:], - h.incomingHTLCExpiryHeight, budget) - _, err := h.Sweeper.SweepInput( inp, sweep.Params{ @@ -551,21 +532,15 @@ func (h *htlcTimeoutResolver) sweepSecondLevelTx() error { // resolveSecondLevelTxLegacy sends a second level timeout transaction to the // utxo nursery. This transaction uses the legacy SIGHASH_ALL flag. func (h *htlcTimeoutResolver) resolveSecondLevelTxLegacy() error { - log.Debugf("%T(%v): incubating htlc output", h, - h.htlcResolution.ClaimOutpoint) + h.log.Debug("incubating htlc output") - err := h.IncubateOutputs( + // The utxo nursery will take care of broadcasting the second-level + // timeout tx and sweeping its output once it confirms. + return h.IncubateOutputs( h.ChanPoint, fn.Some(h.htlcResolution), fn.None[lnwallet.IncomingHtlcResolution](), h.broadcastHeight, h.incomingHTLCExpiryHeight, ) - if err != nil { - return err - } - - h.outputIncubating = true - - return h.Checkpoint(h) } // sweepDirectHtlcOutput sends the direct spend of the HTLC output to the @@ -635,7 +610,7 @@ func (h *htlcTimeoutResolver) spendHtlcOutput() ( // (the case for anchor type channels). In this case we can re-sign it // and attach fees at will. We let the sweeper handle this job. case h.isZeroFeeOutput() && !h.outputIncubating: - if err := h.sweepSecondLevelTx(); err != nil { + if err := h.sweepTimeoutTx(); err != nil { log.Errorf("Sending timeout tx to sweeper: %v", err) return nil, err @@ -696,9 +671,6 @@ func (h *htlcTimeoutResolver) watchHtlcSpend() (*chainntnfs.SpendDetail, func (h *htlcTimeoutResolver) waitForConfirmedSpend(op *wire.OutPoint, pkScript []byte) (*chainntnfs.SpendDetail, error) { - log.Infof("%T(%v): waiting for spent of HTLC output %v to be "+ - "fully confirmed", h, h.htlcResolution.ClaimOutpoint, op) - // We'll block here until either we exit, or the HTLC output on the // commitment transaction has been spent. spend, err := waitForSpend( @@ -770,82 +742,11 @@ func (h *htlcTimeoutResolver) handleCommitSpend( // the CSV and possible CLTV lock to expire, before sweeping the output // on the second-level. case h.isZeroFeeOutput(): - waitHeight := h.deriveWaitHeight( - h.htlcResolution.CsvDelay, commitSpend, - ) - - h.reportLock.Lock() - h.currentReport.Stage = 2 - h.currentReport.MaturityHeight = waitHeight - h.reportLock.Unlock() - - if h.hasCLTV() { - log.Infof("%T(%x): waiting for CSV and CLTV lock to "+ - "expire at height %v", h, h.htlc.RHash[:], - waitHeight) - } else { - log.Infof("%T(%x): waiting for CSV lock to expire at "+ - "height %v", 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, - } - - var csvWitnessType input.StandardWitnessType - if h.isTaproot() { - //nolint:ll - csvWitnessType = input.TaprootHtlcOfferedTimeoutSecondLevel - } else { - csvWitnessType = input.HtlcOfferedTimeoutSecondLevel - } - - // Let the sweeper sweep the second-level output now that the - // CSV/CLTV locks have expired. - inp := h.makeSweepInput( - op, csvWitnessType, - input.LeaseHtlcOfferedTimeoutSecondLevel, - &h.htlcResolution.SweepSignDesc, - h.htlcResolution.CsvDelay, - uint32(commitSpend.SpendingHeight), h.htlc.RHash, - h.htlcResolution.ResolutionBlob, - ) - - // Calculate the budget for this sweep. - budget := calculateBudget( - btcutil.Amount(inp.SignDesc().Output.Value), - h.Budget.NoDeadlineHTLCRatio, - h.Budget.NoDeadlineHTLC, - ) - - log.Infof("%T(%x): offering second-level timeout tx output to "+ - "sweeper with no deadline and budget=%v at height=%v", - h, h.htlc.RHash[:], budget, waitHeight) - - _, err := h.Sweeper.SweepInput( - inp, - sweep.Params{ - Budget: budget, - - // For second level success tx, there's no rush - // to get it confirmed, so we use a nil - // deadline. - DeadlineHeight: fn.None[int32](), - }, - ) + err := h.sweepTimeoutTxOutput() if err != nil { return nil, err } - // Update the claim outpoint to point to the second-level - // transaction created by the sweeper. - claimOutpoint = *op fallthrough // Finally, if this was an output on our commitment transaction, we'll @@ -1288,3 +1189,131 @@ func (h *htlcTimeoutResolver) isLegacyOutput() bool { return h.htlcResolution.SignedTimeoutTx != nil && h.htlcResolution.SignDetails == nil } + +// waitHtlcSpendAndCheckPreimage waits for the htlc output to be spent and +// checks whether the spending reveals the preimage. If the preimage is found, +// it will be added to the preimage beacon to settle the incoming link, and a +// nil spend details will be returned. Otherwise, the spend details will be +// returned, indicating this is a non-preimage spend. +func (h *htlcTimeoutResolver) waitHtlcSpendAndCheckPreimage() ( + *chainntnfs.SpendDetail, error) { + + // Wait for the htlc output to be spent, which can happen in one of the + // paths, + // 1. The remote party spends the htlc output using the preimage. + // 2. The local party spends the htlc timeout tx from the local + // commitment. + // 3. The local party spends the htlc output directlt from the remote + // commitment. + spend, err := h.watchHtlcSpend() + if err != nil { + return nil, err + } + + // If the spend reveals the pre-image, then we'll enter the clean up + // workflow to pass the preimage back to the incoming link, add it to + // the witness cache, and exit. + if isPreimageSpend(h.isTaproot(), spend, !h.isRemoteCommitOutput()) { + return nil, h.claimCleanUp(spend) + } + + return spend, nil +} + +// sweepTimeoutTxOutput attempts to sweep the output of the second level +// timeout tx. +func (h *htlcTimeoutResolver) sweepTimeoutTxOutput() error { + h.log.Debugf("sweeping output %v from 2nd-level HTLC timeout tx", + h.htlcResolution.ClaimOutpoint) + + // This should be non-blocking as we will only attempt to sweep the + // output when the second level tx has already been confirmed. In other + // words, waitHtlcSpendAndCheckPreimage will return immediately. + commitSpend, err := h.waitHtlcSpendAndCheckPreimage() + if err != nil { + return err + } + + // Exit early if the spend is nil, as this means it's a remote spend + // using the preimage path, which is handled in claimCleanUp. + if commitSpend == nil { + h.log.Infof("preimage spend detected, skipping 2nd-level " + + "HTLC output sweep") + + return nil + } + + waitHeight := h.deriveWaitHeight(h.htlcResolution.CsvDelay, commitSpend) + + // Now that the sweeper has broadcasted the second-level transaction, + // it has confirmed, and we have checkpointed our state, we'll sweep + // the second level output. We report the resolver has moved the next + // stage. + h.reportLock.Lock() + h.currentReport.Stage = 2 + h.currentReport.MaturityHeight = waitHeight + h.reportLock.Unlock() + + if h.hasCLTV() { + h.log.Infof("waiting for CSV and CLTV lock to expire at "+ + "height %v", waitHeight) + } else { + h.log.Infof("waiting for CSV lock to expire at height %v", + 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, + } + + var witType input.StandardWitnessType + if h.isTaproot() { + witType = input.TaprootHtlcOfferedTimeoutSecondLevel + } else { + witType = input.HtlcOfferedTimeoutSecondLevel + } + + // Let the sweeper sweep the second-level output now that the CSV/CLTV + // locks have expired. + inp := h.makeSweepInput( + op, witType, + input.LeaseHtlcOfferedTimeoutSecondLevel, + &h.htlcResolution.SweepSignDesc, + h.htlcResolution.CsvDelay, uint32(commitSpend.SpendingHeight), + h.htlc.RHash, h.htlcResolution.ResolutionBlob, + ) + + // Calculate the budget for this sweep. + budget := calculateBudget( + btcutil.Amount(inp.SignDesc().Output.Value), + h.Budget.NoDeadlineHTLCRatio, + h.Budget.NoDeadlineHTLC, + ) + + h.log.Infof("offering output from 2nd-level timeout tx to sweeper "+ + "with no deadline and budget=%v", budget) + + // TODO(yy): use the result chan returned from SweepInput to get the + // confirmation status of this sweeping tx so we don't need to make + // anothe subscription via `RegisterSpendNtfn` for this outpoint here + // in the resolver. + _, err = h.Sweeper.SweepInput( + inp, + sweep.Params{ + Budget: budget, + + // For second level success tx, there's no rush + // to get it confirmed, so we use a nil + // deadline. + DeadlineHeight: fn.None[int32](), + }, + ) + + return err +} From 7083302fa015134faffa303badb3bb65e3668b4c Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 16 Jul 2024 08:44:53 +0800 Subject: [PATCH 38/92] contractcourt: add methods to checkpoint states This commit adds checkpoint methods in `htlcTimeoutResolver`, which are similar to those used in `htlcSuccessResolver`. --- contractcourt/htlc_timeout_resolver.go | 168 +++++++++++-------------- 1 file changed, 71 insertions(+), 97 deletions(-) diff --git a/contractcourt/htlc_timeout_resolver.go b/contractcourt/htlc_timeout_resolver.go index d17059364cf..f2f7e769983 100644 --- a/contractcourt/htlc_timeout_resolver.go +++ b/contractcourt/htlc_timeout_resolver.go @@ -7,6 +7,7 @@ import ( "sync" "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/davecgh/go-spew/spew" @@ -451,26 +452,6 @@ func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) { return nil, h.claimCleanUp(commitSpend) } - // At this point, the second-level transaction is sufficiently - // confirmed, or a transaction directly spending the output is. - // Therefore, we can now send back our clean up message, failing the - // HTLC on the incoming link. - // - // NOTE: This can be called twice if the outgoing resolver restarts - // before the second-stage timeout transaction is confirmed. - log.Infof("%T(%v): resolving htlc with incoming fail msg, "+ - "fully confirmed", h, h.htlcResolution.ClaimOutpoint) - - failureMsg := &lnwire.FailPermanentChannelFailure{} - err = h.DeliverResolutionMsg(ResolutionMsg{ - SourceChan: h.ShortChanID, - HtlcIndex: h.htlc.HtlcIndex, - Failure: failureMsg, - }) - if err != nil { - return nil, err - } - // Depending on whether this was a local or remote commit, we must // handle the spending transaction accordingly. return h.handleCommitSpend(commitSpend) @@ -680,30 +661,9 @@ func (h *htlcTimeoutResolver) waitForConfirmedSpend(op *wire.OutPoint, return nil, err } - // Once confirmed, persist the state on disk. - if err := h.checkPointSecondLevelTx(); err != nil { - return nil, err - } - return spend, err } -// checkPointSecondLevelTx persists the state of a second level HTLC tx to disk -// if it's published by the sweeper. -func (h *htlcTimeoutResolver) checkPointSecondLevelTx() error { - // If this was the second level transaction published by the sweeper, - // we can checkpoint the resolver now that it's confirmed. - if h.htlcResolution.SignDetails != nil && !h.outputIncubating { - h.outputIncubating = true - if err := h.Checkpoint(h); err != nil { - log.Errorf("unable to Checkpoint: %v", err) - return err - } - } - - return nil -} - // handleCommitSpend handles the spend of the HTLC output on the commitment // transaction. If this was our local commitment, the spend will be he // confirmed second-level timeout transaction, and we'll sweep that into our @@ -727,7 +687,8 @@ func (h *htlcTimeoutResolver) handleCommitSpend( // accordingly. spendTxID = commitSpend.SpenderTxHash - reports []*channeldb.ResolverReport + sweepTx *chainntnfs.SpendDetail + err error ) switch { @@ -756,7 +717,7 @@ func (h *htlcTimeoutResolver) handleCommitSpend( log.Infof("%T(%v): waiting for nursery/sweeper to spend CSV "+ "delayed output", h, claimOutpoint) - sweepTx, err := waitForSpend( + sweepTx, err = waitForSpend( &claimOutpoint, h.htlcResolution.SweepSignDesc.Output.PkScript, h.broadcastHeight, h.Notifier, h.quit, @@ -770,38 +731,16 @@ func (h *htlcTimeoutResolver) handleCommitSpend( // Once our sweep of the timeout tx has confirmed, we add a // resolution for our timeoutTx tx first stage transaction. - timeoutTx := commitSpend.SpendingTx - index := commitSpend.SpenderInputIndex - spendHash := commitSpend.SpenderTxHash - - reports = append(reports, &channeldb.ResolverReport{ - OutPoint: timeoutTx.TxIn[index].PreviousOutPoint, - Amount: h.htlc.Amt.ToSatoshis(), - ResolverType: channeldb.ResolverTypeOutgoingHtlc, - ResolverOutcome: channeldb.ResolverOutcomeFirstStage, - SpendTxID: spendHash, - }) + err = h.checkpointStageOne(*spendTxID) + if err != nil { + return nil, err + } } // With the clean up message sent, we'll now mark the contract // resolved, update the recovered balance, record the timeout and the // sweep txid on disk, and wait. - h.resolved = true - h.reportLock.Lock() - h.currentReport.RecoveredBalance = h.currentReport.LimboBalance - h.currentReport.LimboBalance = 0 - h.reportLock.Unlock() - - amt := btcutil.Amount(h.htlcResolution.SweepSignDesc.Output.Value) - reports = append(reports, &channeldb.ResolverReport{ - OutPoint: claimOutpoint, - Amount: amt, - ResolverType: channeldb.ResolverTypeOutgoingHtlc, - ResolverOutcome: channeldb.ResolverOutcomeTimeout, - SpendTxID: spendTxID, - }) - - return nil, h.Checkpoint(h, reports...) + return nil, h.checkpointClaim(sweepTx) } // Stop signals the resolver to cancel any current resolution processes, and @@ -1050,12 +989,6 @@ func (h *htlcTimeoutResolver) consumeSpendEvents(resultChan chan *spendResult, // Create a result chan to hold the results. result := &spendResult{} - // hasMempoolSpend is a flag that indicates whether we have found a - // preimage spend from the mempool. This is used to determine whether - // to checkpoint the resolver or not when later we found the - // corresponding block spend. - hasMempoolSpent := false - // Wait for a spend event to arrive. for { select { @@ -1083,23 +1016,6 @@ func (h *htlcTimeoutResolver) consumeSpendEvents(resultChan chan *spendResult, // Once confirmed, persist the state on disk if // we haven't seen the output's spending tx in // mempool before. - // - // NOTE: we don't checkpoint the resolver if - // it's spending tx has already been found in - // mempool - the resolver will take care of the - // checkpoint in its `claimCleanUp`. If we do - // checkpoint here, however, we'd create a new - // record in db for the same htlc resolver - // which won't be cleaned up later, resulting - // the channel to stay in unresolved state. - // - // TODO(yy): when fee bumper is implemented, we - // need to further check whether this is a - // preimage spend. Also need to refactor here - // to save us some indentation. - if !hasMempoolSpent { - result.err = h.checkPointSecondLevelTx() - } } // Send the result and exit the loop. @@ -1146,10 +1062,6 @@ func (h *htlcTimeoutResolver) consumeSpendEvents(resultChan chan *spendResult, result.spend = spendDetail resultChan <- result - // Set the hasMempoolSpent flag to true so we won't - // checkpoint the resolver again in db. - hasMempoolSpent = true - continue // If the resolver exits, we exit the goroutine. @@ -1317,3 +1229,65 @@ func (h *htlcTimeoutResolver) sweepTimeoutTxOutput() error { return err } + +// checkpointStageOne creates a checkpoint for the first stage of the htlc +// timeout transaction. This is used to ensure that the resolver can resume +// watching for the second stage spend in case of a restart. +func (h *htlcTimeoutResolver) checkpointStageOne( + spendTxid chainhash.Hash) error { + + h.log.Debugf("checkpoint stage one spend of HTLC output %v, spent "+ + "in tx %v", h.outpoint(), spendTxid) + + // Now that the second-level transaction has confirmed, we checkpoint + // the state so we'll go to the next stage in case of restarts. + h.outputIncubating = true + + // Create stage-one report. + report := &channeldb.ResolverReport{ + OutPoint: h.outpoint(), + Amount: h.htlc.Amt.ToSatoshis(), + ResolverType: channeldb.ResolverTypeOutgoingHtlc, + ResolverOutcome: channeldb.ResolverOutcomeFirstStage, + SpendTxID: &spendTxid, + } + + // At this point, the second-level transaction is sufficiently + // confirmed. We can now send back our clean up message, failing the + // HTLC on the incoming link. + failureMsg := &lnwire.FailPermanentChannelFailure{} + err := h.DeliverResolutionMsg(ResolutionMsg{ + SourceChan: h.ShortChanID, + HtlcIndex: h.htlc.HtlcIndex, + Failure: failureMsg, + }) + if err != nil { + return err + } + + return h.Checkpoint(h, report) +} + +// checkpointClaim checkpoints the timeout resolver with the reports it needs. +func (h *htlcTimeoutResolver) checkpointClaim( + spendDetail *chainntnfs.SpendDetail) error { + + h.log.Infof("resolving htlc with incoming fail msg, output=%v "+ + "confirmed in tx=%v", spendDetail.SpentOutPoint, + spendDetail.SpenderTxHash) + + // Create a resolver report for the claiming of the HTLC. + amt := btcutil.Amount(h.htlcResolution.SweepSignDesc.Output.Value) + report := &channeldb.ResolverReport{ + OutPoint: *spendDetail.SpentOutPoint, + Amount: amt, + ResolverType: channeldb.ResolverTypeOutgoingHtlc, + ResolverOutcome: channeldb.ResolverOutcomeTimeout, + SpendTxID: spendDetail.SpenderTxHash, + } + + // Finally, we checkpoint the resolver with our report(s). + h.resolved = true + + return h.Checkpoint(h, report) +} From 730b605ed462ddf23fd6c9c640a6d0fbaad53633 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 16 Jul 2024 08:53:00 +0800 Subject: [PATCH 39/92] contractcourt: add resolve handlers in `htlcTimeoutResolver` This commit adds more methods to handle resolving the spending of the output based on different spending paths. --- contractcourt/htlc_timeout_resolver.go | 201 +++++++++++++++---------- 1 file changed, 121 insertions(+), 80 deletions(-) diff --git a/contractcourt/htlc_timeout_resolver.go b/contractcourt/htlc_timeout_resolver.go index f2f7e769983..9904f37d3d7 100644 --- a/contractcourt/htlc_timeout_resolver.go +++ b/contractcourt/htlc_timeout_resolver.go @@ -454,7 +454,11 @@ func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) { // Depending on whether this was a local or remote commit, we must // handle the spending transaction accordingly. - return h.handleCommitSpend(commitSpend) + if h.isRemoteCommitOutput() { + return nil, h.resolveRemoteCommitOutput() + } + + return nil, h.resolveTimeoutTx() } // sweepTimeoutTx sends a second level timeout transaction to the sweeper. @@ -664,85 +668,6 @@ func (h *htlcTimeoutResolver) waitForConfirmedSpend(op *wire.OutPoint, return spend, err } -// handleCommitSpend handles the spend of the HTLC output on the commitment -// transaction. If this was our local commitment, the spend will be he -// confirmed second-level timeout transaction, and we'll sweep that into our -// wallet. If the was a remote commitment, the resolver will resolve -// immetiately. -func (h *htlcTimeoutResolver) handleCommitSpend( - commitSpend *chainntnfs.SpendDetail) (ContractResolver, error) { - - var ( - // claimOutpoint will be the outpoint of the second level - // transaction, or on the remote commitment directly. It will - // start out as set in the resolution, but we'll update it if - // the second-level goes through the sweeper and changes its - // txid. - claimOutpoint = h.htlcResolution.ClaimOutpoint - - // spendTxID will be the ultimate spend of the claimOutpoint. - // We set it to the commit spend for now, as this is the - // ultimate spend in case this is a remote commitment. If we go - // through the second-level transaction, we'll update this - // accordingly. - spendTxID = commitSpend.SpenderTxHash - - sweepTx *chainntnfs.SpendDetail - err error - ) - - switch { - - // If we swept an HTLC directly off the remote party's commitment - // transaction, then we can exit here as there's no second level sweep - // to do. - case h.htlcResolution.SignedTimeoutTx == nil: - break - - // If the sweeper is handling the second level transaction, wait for - // the CSV and possible CLTV lock to expire, before sweeping the output - // on the second-level. - case h.isZeroFeeOutput(): - err := h.sweepTimeoutTxOutput() - if err != nil { - return nil, err - } - - fallthrough - - // Finally, if this was an output on our commitment transaction, we'll - // wait for the second-level HTLC output to be spent, and for that - // transaction itself to confirm. - case !h.isRemoteCommitOutput(): - log.Infof("%T(%v): waiting for nursery/sweeper to spend CSV "+ - "delayed output", h, claimOutpoint) - - sweepTx, err = waitForSpend( - &claimOutpoint, - h.htlcResolution.SweepSignDesc.Output.PkScript, - h.broadcastHeight, h.Notifier, h.quit, - ) - if err != nil { - return nil, err - } - - // Update the spend txid to the hash of the sweep transaction. - spendTxID = sweepTx.SpenderTxHash - - // Once our sweep of the timeout tx has confirmed, we add a - // resolution for our timeoutTx tx first stage transaction. - err = h.checkpointStageOne(*spendTxID) - if err != nil { - return nil, err - } - } - - // With the clean up message sent, we'll now mark the contract - // resolved, update the recovered balance, record the timeout and the - // sweep txid on disk, and wait. - return nil, h.checkpointClaim(sweepTx) -} - // Stop signals the resolver to cancel any current resolution processes, and // suspend. // @@ -1291,3 +1216,119 @@ func (h *htlcTimeoutResolver) checkpointClaim( return h.Checkpoint(h, report) } + +// resolveRemoteCommitOutput handles sweeping an HTLC output on the remote +// commitment with via the timeout path. In this case we can sweep the output +// directly, and don't have to broadcast a second-level transaction. +func (h *htlcTimeoutResolver) resolveRemoteCommitOutput() error { + h.log.Debug("waiting for direct-timeout spend of the htlc to confirm") + + // Wait for the direct-timeout HTLC sweep tx to confirm. + spend, err := h.watchHtlcSpend() + if err != nil { + return err + } + + // If the spend reveals the preimage, then we'll enter the clean up + // workflow to pass the preimage back to the incoming link, add it to + // the witness cache, and exit. + if isPreimageSpend(h.isTaproot(), spend, !h.isRemoteCommitOutput()) { + return h.claimCleanUp(spend) + } + + // Send the clean up msg to fail the incoming HTLC. + failureMsg := &lnwire.FailPermanentChannelFailure{} + err = h.DeliverResolutionMsg(ResolutionMsg{ + SourceChan: h.ShortChanID, + HtlcIndex: h.htlc.HtlcIndex, + Failure: failureMsg, + }) + if err != nil { + return err + } + + // TODO(yy): should also update the `RecoveredBalance` and + // `LimboBalance` like other paths? + + // Checkpoint the resolver, and write the outcome to disk. + return h.checkpointClaim(spend) +} + +// resolveTimeoutTx waits for the sweeping tx of the second-level +// timeout tx to confirm and offers the output from the timeout tx to the +// sweeper. +func (h *htlcTimeoutResolver) resolveTimeoutTx() error { + h.log.Debug("waiting for first-stage 2nd-level HTLC timeout tx to " + + "confirm") + + // Wait for the second level transaction to confirm. + spend, err := h.watchHtlcSpend() + if err != nil { + return err + } + + // If the spend reveals the preimage, then we'll enter the clean up + // workflow to pass the preimage back to the incoming link, add it to + // the witness cache, and exit. + if isPreimageSpend(h.isTaproot(), spend, !h.isRemoteCommitOutput()) { + return h.claimCleanUp(spend) + } + + op := h.htlcResolution.ClaimOutpoint + spenderTxid := *spend.SpenderTxHash + + // If the timeout tx is a re-signed tx, we will need to find the actual + // spent outpoint from the spending tx. + if h.isZeroFeeOutput() { + op = wire.OutPoint{ + Hash: spenderTxid, + Index: spend.SpenderInputIndex, + } + } + + // If the 2nd-stage sweeping has already been started, we can + // fast-forward to start the resolving process for the stage two + // output. + if h.outputIncubating { + return h.resolveTimeoutTxOutput(op) + } + + h.log.Infof("2nd-level HTLC timeout tx=%v confirmed", spenderTxid) + + // Start the process to sweep the output from the timeout tx. + err = h.sweepTimeoutTxOutput() + if err != nil { + return err + } + + // Create a checkpoint since the timeout tx is confirmed and the sweep + // request has been made. + if err := h.checkpointStageOne(spenderTxid); err != nil { + return err + } + + // Start the resolving process for the stage two output. + return h.resolveTimeoutTxOutput(op) +} + +// resolveTimeoutTxOutput waits for the spend of the output from the 2nd-level +// timeout tx. +func (h *htlcTimeoutResolver) resolveTimeoutTxOutput(op wire.OutPoint) error { + h.log.Debugf("waiting for second-stage 2nd-level timeout tx output %v "+ + "to be spent after csv_delay=%v", op, h.htlcResolution.CsvDelay) + + spend, err := waitForSpend( + &op, h.htlcResolution.SweepSignDesc.Output.PkScript, + h.broadcastHeight, h.Notifier, h.quit, + ) + if err != nil { + return err + } + + h.reportLock.Lock() + h.currentReport.RecoveredBalance = h.currentReport.LimboBalance + h.currentReport.LimboBalance = 0 + h.reportLock.Unlock() + + return h.checkpointClaim(spend) +} From a98763494f9d43978609f416797d13366527fd6e Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Mon, 24 Jun 2024 21:49:21 +0800 Subject: [PATCH 40/92] contractcourt: add `Launch` method to anchor/breach resolver We will use this and its following commits to break the original `Resolve` methods into two parts - the first part is moved to a new method `Launch`, which handles sending a sweep request to the sweeper. The second part remains in `Resolve`, which is mainly waiting for a spending tx. Breach resolver currently doesn't do anything in its `Launch` since the sweeping of justice outputs are not handled by the sweeper yet. --- contractcourt/anchor_resolver.go | 121 +++++++++++++++++++---------- contractcourt/breach_resolver.go | 21 +++++ contractcourt/contract_resolver.go | 10 +++ 3 files changed, 109 insertions(+), 43 deletions(-) diff --git a/contractcourt/anchor_resolver.go b/contractcourt/anchor_resolver.go index b50e061f447..ded3ebd83f2 100644 --- a/contractcourt/anchor_resolver.go +++ b/contractcourt/anchor_resolver.go @@ -84,49 +84,14 @@ func (c *anchorResolver) ResolverKey() []byte { return nil } -// Resolve offers the anchor output to the sweeper and waits for it to be swept. +// Resolve waits for the output to be swept. +// +// NOTE: Part of the ContractResolver interface. func (c *anchorResolver) Resolve() (ContractResolver, error) { - // Attempt to update the sweep parameters to the post-confirmation - // situation. We don't want to force sweep anymore, because the anchor - // lost its special purpose to get the commitment confirmed. It is just - // an output that we want to sweep only if it is economical to do so. - // - // An exclusive group is not necessary anymore, because we know that - // this is the only anchor that can be swept. - // - // We also clear the parent tx information for cpfp, because the - // commitment tx is confirmed. - // - // After a restart or when the remote force closes, the sweeper is not - // yet aware of the anchor. In that case, it will be added as new input - // to the sweeper. - witnessType := input.CommitmentAnchor - - // For taproot channels, we need to use the proper witness type. - if c.chanType.IsTaproot() { - witnessType = input.TaprootAnchorSweepSpend - } - - anchorInput := input.MakeBaseInput( - &c.anchor, witnessType, &c.anchorSignDescriptor, - c.broadcastHeight, nil, - ) - - resultChan, err := c.Sweeper.SweepInput( - &anchorInput, - sweep.Params{ - // For normal anchor sweeping, the budget is 330 sats. - Budget: btcutil.Amount( - anchorInput.SignDesc().Output.Value, - ), - - // There's no rush to sweep the anchor, so we use a nil - // deadline here. - DeadlineHeight: fn.None[int32](), - }, - ) - if err != nil { - return nil, err + // If we're already resolved, then we can exit early. + if c.resolved { + c.log.Errorf("already resolved") + return nil, nil } var ( @@ -135,7 +100,7 @@ func (c *anchorResolver) Resolve() (ContractResolver, error) { ) select { - case sweepRes := <-resultChan: + case sweepRes := <-c.sweepResultChan: switch sweepRes.Err { // Anchor was swept successfully. case nil: @@ -161,6 +126,8 @@ func (c *anchorResolver) Resolve() (ContractResolver, error) { return nil, errResolverShuttingDown } + c.log.Infof("resolved in tx %v", spendTx) + // Update report to reflect that funds are no longer in limbo. c.reportLock.Lock() if outcome == channeldb.ResolverOutcomeClaimed { @@ -181,6 +148,9 @@ func (c *anchorResolver) Resolve() (ContractResolver, error) { // // NOTE: Part of the ContractResolver interface. func (c *anchorResolver) Stop() { + c.log.Debugf("stopping...") + defer c.log.Debugf("stopped") + close(c.quit) } @@ -216,3 +186,68 @@ func (c *anchorResolver) Encode(w io.Writer) error { // A compile time assertion to ensure anchorResolver meets the // ContractResolver interface. var _ ContractResolver = (*anchorResolver)(nil) + +// Launch offers the anchor output to the sweeper. +func (c *anchorResolver) Launch() error { + if c.launched { + c.log.Tracef("already launched") + return nil + } + + c.log.Debugf("launching resolver...") + c.launched = true + + // If we're already resolved, then we can exit early. + if c.resolved { + c.log.Errorf("already resolved") + return nil + } + + // Attempt to update the sweep parameters to the post-confirmation + // situation. We don't want to force sweep anymore, because the anchor + // lost its special purpose to get the commitment confirmed. It is just + // an output that we want to sweep only if it is economical to do so. + // + // An exclusive group is not necessary anymore, because we know that + // this is the only anchor that can be swept. + // + // We also clear the parent tx information for cpfp, because the + // commitment tx is confirmed. + // + // After a restart or when the remote force closes, the sweeper is not + // yet aware of the anchor. In that case, it will be added as new input + // to the sweeper. + witnessType := input.CommitmentAnchor + + // For taproot channels, we need to use the proper witness type. + if c.chanType.IsTaproot() { + witnessType = input.TaprootAnchorSweepSpend + } + + anchorInput := input.MakeBaseInput( + &c.anchor, witnessType, &c.anchorSignDescriptor, + c.broadcastHeight, nil, + ) + + resultChan, err := c.Sweeper.SweepInput( + &anchorInput, + sweep.Params{ + // For normal anchor sweeping, the budget is 330 sats. + Budget: btcutil.Amount( + anchorInput.SignDesc().Output.Value, + ), + + // There's no rush to sweep the anchor, so we use a nil + // deadline here. + DeadlineHeight: fn.None[int32](), + }, + ) + + if err != nil { + return err + } + + c.sweepResultChan = resultChan + + return nil +} diff --git a/contractcourt/breach_resolver.go b/contractcourt/breach_resolver.go index 9a5f4bbe08e..1f74924d397 100644 --- a/contractcourt/breach_resolver.go +++ b/contractcourt/breach_resolver.go @@ -47,6 +47,8 @@ func (b *breachResolver) ResolverKey() []byte { // Resolve queries the BreachArbitrator to see if the justice transaction has // been broadcast. // +// NOTE: Part of the ContractResolver interface. +// // TODO(yy): let sweeper handle the breach inputs. func (b *breachResolver) Resolve() (ContractResolver, error) { if !b.subscribed { @@ -83,6 +85,7 @@ func (b *breachResolver) Resolve() (ContractResolver, error) { // Stop signals the breachResolver to stop. func (b *breachResolver) Stop() { + b.log.Debugf("stopping...") close(b.quit) } @@ -123,3 +126,21 @@ func newBreachResolverFromReader(r io.Reader, resCfg ResolverConfig) ( // A compile time assertion to ensure breachResolver meets the ContractResolver // interface. var _ ContractResolver = (*breachResolver)(nil) + +// Launch offers the breach outputs to the sweeper - currently it's a NOOP as +// the outputs here are not offered to the sweeper. +// +// NOTE: Part of the ContractResolver interface. +// +// TODO(yy): implement it once the outputs are offered to the sweeper. +func (b *breachResolver) Launch() error { + if b.launched { + b.log.Tracef("already launched") + return nil + } + + b.log.Debugf("launching resolver...") + b.launched = true + + return nil +} diff --git a/contractcourt/contract_resolver.go b/contractcourt/contract_resolver.go index ff52ce9760d..814c02ff564 100644 --- a/contractcourt/contract_resolver.go +++ b/contractcourt/contract_resolver.go @@ -10,6 +10,7 @@ import ( "github.com/btcsuite/btclog/v2" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/fn/v2" + "github.com/lightningnetwork/lnd/sweep" ) var ( @@ -109,6 +110,15 @@ type contractResolverKit struct { log btclog.Logger quit chan struct{} + + // sweepResultChan is the result chan returned from calling + // `SweepInput`. It should be mounted to the specific resolver once the + // input has been offered to the sweeper. + sweepResultChan chan sweep.Result + + // launched specifies whether the resolver has been launched. Calling + // `Launch` will be a no-op if this is true. + launched bool } // newContractResolverKit instantiates the mix-in struct. From 913f5d4657631166527a609963d6147d516a506e Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 20 Jun 2024 22:05:33 +0800 Subject: [PATCH 41/92] contractcourt: add `Launch` method to commit resolver --- contractcourt/commit_sweep_resolver.go | 339 +++++++++++--------- contractcourt/commit_sweep_resolver_test.go | 4 + 2 files changed, 186 insertions(+), 157 deletions(-) diff --git a/contractcourt/commit_sweep_resolver.go b/contractcourt/commit_sweep_resolver.go index 7b101f80e33..b3323158db0 100644 --- a/contractcourt/commit_sweep_resolver.go +++ b/contractcourt/commit_sweep_resolver.go @@ -172,165 +172,10 @@ func (c *commitSweepResolver) getCommitTxConfHeight() (uint32, error) { func (c *commitSweepResolver) Resolve() (ContractResolver, error) { // If we're already resolved, then we can exit early. if c.resolved { + c.log.Errorf("already resolved") return nil, nil } - confHeight, err := c.getCommitTxConfHeight() - if err != nil { - return nil, err - } - - // Wait up until the CSV expires, unless we also have a CLTV that - // expires after. - unlockHeight := confHeight + c.commitResolution.MaturityDelay - if c.hasCLTV() { - unlockHeight = uint32(math.Max( - float64(unlockHeight), float64(c.leaseExpiry), - )) - } - - c.log.Debugf("commit conf_height=%v, unlock_height=%v", - confHeight, unlockHeight) - - // Update report now that we learned the confirmation height. - c.reportLock.Lock() - c.currentReport.MaturityHeight = unlockHeight - c.reportLock.Unlock() - - var ( - isLocalCommitTx bool - - signDesc = c.commitResolution.SelfOutputSignDesc - ) - - switch { - // For taproot channels, we'll know if this is the local commit based - // on the timelock value. For remote commitment transactions, the - // witness script has a timelock of 1. - case c.chanType.IsTaproot(): - delayKey := c.localChanCfg.DelayBasePoint.PubKey - nonDelayKey := c.localChanCfg.PaymentBasePoint.PubKey - - signKey := c.commitResolution.SelfOutputSignDesc.KeyDesc.PubKey - - // If the key in the script is neither of these, we shouldn't - // proceed. This should be impossible. - if !signKey.IsEqual(delayKey) && !signKey.IsEqual(nonDelayKey) { - return nil, fmt.Errorf("unknown sign key %v", signKey) - } - - // The commitment transaction is ours iff the signing key is - // the delay key. - isLocalCommitTx = signKey.IsEqual(delayKey) - - // The output is on our local commitment if the script starts with - // OP_IF for the revocation clause. On the remote commitment it will - // either be a regular P2WKH or a simple sig spend with a CSV delay. - default: - isLocalCommitTx = signDesc.WitnessScript[0] == txscript.OP_IF - } - isDelayedOutput := c.commitResolution.MaturityDelay != 0 - - c.log.Debugf("isDelayedOutput=%v, isLocalCommitTx=%v", isDelayedOutput, - isLocalCommitTx) - - // There're three types of commitments, those that have tweaks for the - // remote key (us in this case), those that don't, and a third where - // there is no tweak and the output is delayed. On the local commitment - // our output will always be delayed. We'll rely on the presence of the - // commitment tweak to discern which type of commitment this is. - var witnessType input.WitnessType - switch { - // The local delayed output for a taproot channel. - case isLocalCommitTx && c.chanType.IsTaproot(): - witnessType = input.TaprootLocalCommitSpend - - // The CSV 1 delayed output for a taproot channel. - case !isLocalCommitTx && c.chanType.IsTaproot(): - witnessType = input.TaprootRemoteCommitSpend - - // Delayed output to us on our local commitment for a channel lease in - // which we are the initiator. - case isLocalCommitTx && c.hasCLTV(): - witnessType = input.LeaseCommitmentTimeLock - - // Delayed output to us on our local commitment. - case isLocalCommitTx: - witnessType = input.CommitmentTimeLock - - // A confirmed output to us on the remote commitment for a channel lease - // in which we are the initiator. - case isDelayedOutput && c.hasCLTV(): - witnessType = input.LeaseCommitmentToRemoteConfirmed - - // A confirmed output to us on the remote commitment. - case isDelayedOutput: - witnessType = input.CommitmentToRemoteConfirmed - - // A non-delayed output on the remote commitment where the key is - // tweakless. - case c.commitResolution.SelfOutputSignDesc.SingleTweak == nil: - witnessType = input.CommitSpendNoDelayTweakless - - // A non-delayed output on the remote commitment where the key is - // tweaked. - default: - witnessType = input.CommitmentNoDelay - } - - c.log.Infof("Sweeping with witness type: %v", witnessType) - - // We'll craft an input with all the information required for the - // sweeper to create a fully valid sweeping transaction to recover - // these coins. - var inp *input.BaseInput - if c.hasCLTV() { - inp = input.NewCsvInputWithCltv( - &c.commitResolution.SelfOutPoint, witnessType, - &c.commitResolution.SelfOutputSignDesc, - c.broadcastHeight, c.commitResolution.MaturityDelay, - c.leaseExpiry, - input.WithResolutionBlob( - c.commitResolution.ResolutionBlob, - ), - ) - } else { - inp = input.NewCsvInput( - &c.commitResolution.SelfOutPoint, witnessType, - &c.commitResolution.SelfOutputSignDesc, - c.broadcastHeight, c.commitResolution.MaturityDelay, - input.WithResolutionBlob( - c.commitResolution.ResolutionBlob, - ), - ) - } - - // TODO(roasbeef): instead of ading ctrl block to the sign desc, make - // new input type, have sweeper set it? - - // Calculate the budget for the sweeping this input. - budget := calculateBudget( - btcutil.Amount(inp.SignDesc().Output.Value), - c.Budget.ToLocalRatio, c.Budget.ToLocal, - ) - c.log.Infof("Sweeping commit output using budget=%v", budget) - - // With our input constructed, we'll now offer it to the sweeper. - resultChan, err := c.Sweeper.SweepInput( - inp, sweep.Params{ - Budget: budget, - - // Specify a nil deadline here as there's no time - // pressure. - DeadlineHeight: fn.None[int32](), - }, - ) - if err != nil { - c.log.Errorf("unable to sweep input: %v", err) - - return nil, err - } - var sweepTxID chainhash.Hash // Sweeper is going to join this input with other inputs if possible @@ -339,7 +184,7 @@ func (c *commitSweepResolver) Resolve() (ContractResolver, error) { // happen. outcome := channeldb.ResolverOutcomeClaimed select { - case sweepResult := <-resultChan: + case sweepResult := <-c.sweepResultChan: switch sweepResult.Err { // If the remote party was able to sweep this output it's // likely what we sent was actually a revoked commitment. @@ -391,6 +236,8 @@ func (c *commitSweepResolver) Resolve() (ContractResolver, error) { // // NOTE: Part of the ContractResolver interface. func (c *commitSweepResolver) Stop() { + c.log.Debugf("stopping...") + defer c.log.Debugf("stopped") close(c.quit) } @@ -524,3 +371,181 @@ func (c *commitSweepResolver) initReport() { // A compile time assertion to ensure commitSweepResolver meets the // ContractResolver interface. var _ reportingContractResolver = (*commitSweepResolver)(nil) + +// Launch constructs a commit input and offers it to the sweeper. +func (c *commitSweepResolver) Launch() error { + if c.launched { + c.log.Tracef("already launched") + return nil + } + + c.log.Debugf("launching resolver...") + c.launched = true + + // If we're already resolved, then we can exit early. + if c.resolved { + c.log.Errorf("already resolved") + return nil + } + + confHeight, err := c.getCommitTxConfHeight() + if err != nil { + return err + } + + // Wait up until the CSV expires, unless we also have a CLTV that + // expires after. + unlockHeight := confHeight + c.commitResolution.MaturityDelay + if c.hasCLTV() { + unlockHeight = uint32(math.Max( + float64(unlockHeight), float64(c.leaseExpiry), + )) + } + + // Update report now that we learned the confirmation height. + c.reportLock.Lock() + c.currentReport.MaturityHeight = unlockHeight + c.reportLock.Unlock() + + // Derive the witness type for this input. + witnessType, err := c.decideWitnessType() + if err != nil { + return err + } + + // We'll craft an input with all the information required for the + // sweeper to create a fully valid sweeping transaction to recover + // these coins. + var inp *input.BaseInput + if c.hasCLTV() { + inp = input.NewCsvInputWithCltv( + &c.commitResolution.SelfOutPoint, witnessType, + &c.commitResolution.SelfOutputSignDesc, + c.broadcastHeight, c.commitResolution.MaturityDelay, + c.leaseExpiry, + ) + } else { + inp = input.NewCsvInput( + &c.commitResolution.SelfOutPoint, witnessType, + &c.commitResolution.SelfOutputSignDesc, + c.broadcastHeight, c.commitResolution.MaturityDelay, + ) + } + + // TODO(roasbeef): instead of ading ctrl block to the sign desc, make + // new input type, have sweeper set it? + + // Calculate the budget for the sweeping this input. + budget := calculateBudget( + btcutil.Amount(inp.SignDesc().Output.Value), + c.Budget.ToLocalRatio, c.Budget.ToLocal, + ) + c.log.Infof("sweeping commit output %v using budget=%v", witnessType, + budget) + + // With our input constructed, we'll now offer it to the sweeper. + resultChan, err := c.Sweeper.SweepInput( + inp, sweep.Params{ + Budget: budget, + + // Specify a nil deadline here as there's no time + // pressure. + DeadlineHeight: fn.None[int32](), + }, + ) + if err != nil { + c.log.Errorf("unable to sweep input: %v", err) + + return err + } + + c.sweepResultChan = resultChan + + return nil +} + +// decideWitnessType returns the witness type for the input. +func (c *commitSweepResolver) decideWitnessType() (input.WitnessType, error) { + var ( + isLocalCommitTx bool + signDesc = c.commitResolution.SelfOutputSignDesc + ) + + switch { + // For taproot channels, we'll know if this is the local commit based + // on the timelock value. For remote commitment transactions, the + // witness script has a timelock of 1. + case c.chanType.IsTaproot(): + delayKey := c.localChanCfg.DelayBasePoint.PubKey + nonDelayKey := c.localChanCfg.PaymentBasePoint.PubKey + + signKey := c.commitResolution.SelfOutputSignDesc.KeyDesc.PubKey + + // If the key in the script is neither of these, we shouldn't + // proceed. This should be impossible. + if !signKey.IsEqual(delayKey) && !signKey.IsEqual(nonDelayKey) { + return nil, fmt.Errorf("unknown sign key %v", signKey) + } + + // The commitment transaction is ours iff the signing key is + // the delay key. + isLocalCommitTx = signKey.IsEqual(delayKey) + + // The output is on our local commitment if the script starts with + // OP_IF for the revocation clause. On the remote commitment it will + // either be a regular P2WKH or a simple sig spend with a CSV delay. + default: + isLocalCommitTx = signDesc.WitnessScript[0] == txscript.OP_IF + } + + isDelayedOutput := c.commitResolution.MaturityDelay != 0 + + c.log.Debugf("isDelayedOutput=%v, isLocalCommitTx=%v", isDelayedOutput, + isLocalCommitTx) + + // There're three types of commitments, those that have tweaks for the + // remote key (us in this case), those that don't, and a third where + // there is no tweak and the output is delayed. On the local commitment + // our output will always be delayed. We'll rely on the presence of the + // commitment tweak to discern which type of commitment this is. + var witnessType input.WitnessType + switch { + // The local delayed output for a taproot channel. + case isLocalCommitTx && c.chanType.IsTaproot(): + witnessType = input.TaprootLocalCommitSpend + + // The CSV 1 delayed output for a taproot channel. + case !isLocalCommitTx && c.chanType.IsTaproot(): + witnessType = input.TaprootRemoteCommitSpend + + // Delayed output to us on our local commitment for a channel lease in + // which we are the initiator. + case isLocalCommitTx && c.hasCLTV(): + witnessType = input.LeaseCommitmentTimeLock + + // Delayed output to us on our local commitment. + case isLocalCommitTx: + witnessType = input.CommitmentTimeLock + + // A confirmed output to us on the remote commitment for a channel lease + // in which we are the initiator. + case isDelayedOutput && c.hasCLTV(): + witnessType = input.LeaseCommitmentToRemoteConfirmed + + // A confirmed output to us on the remote commitment. + case isDelayedOutput: + witnessType = input.CommitmentToRemoteConfirmed + + // A non-delayed output on the remote commitment where the key is + // tweakless. + case c.commitResolution.SelfOutputSignDesc.SingleTweak == nil: + witnessType = input.CommitSpendNoDelayTweakless + + // A non-delayed output on the remote commitment where the key is + // tweaked. + default: + witnessType = input.CommitmentNoDelay + } + + return witnessType, nil +} diff --git a/contractcourt/commit_sweep_resolver_test.go b/contractcourt/commit_sweep_resolver_test.go index 2195e33779f..6855fddcd32 100644 --- a/contractcourt/commit_sweep_resolver_test.go +++ b/contractcourt/commit_sweep_resolver_test.go @@ -15,6 +15,7 @@ import ( "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/sweep" + "github.com/stretchr/testify/require" ) type commitSweepResolverTestContext struct { @@ -82,6 +83,9 @@ func (i *commitSweepResolverTestContext) resolve() { // Start resolver. i.resolverResultChan = make(chan resolveResult, 1) go func() { + err := i.resolver.Launch() + require.NoError(i.t, err) + nextResolver, err := i.resolver.Resolve() i.resolverResultChan <- resolveResult{ nextResolver: nextResolver, From cf105e67f4c45b40b0cb6c0564a5ab8a5e91bf1b Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 16 Jul 2024 07:24:45 +0800 Subject: [PATCH 42/92] contractcourt: add `Launch` method to htlc success resolver This commit breaks the `Resolve` into two parts - the first part is moved into a `Launch` method that handles sending sweep requests, and the second part remains in `Resolve` which handles waiting for the spend. Since we are using both utxo nursery and sweeper at the same time, to make sure this change doesn't break the existing behavior, we implement the `Launch` as following, - zero-fee htlc - handled by the sweeper - direct output from the remote commit - handled by the sweeper - legacy htlc - handled by the utxo nursery --- contractcourt/htlc_success_resolver.go | 186 ++++++++------------ contractcourt/htlc_success_resolver_test.go | 87 +++++++-- 2 files changed, 152 insertions(+), 121 deletions(-) diff --git a/contractcourt/htlc_success_resolver.go b/contractcourt/htlc_success_resolver.go index b436ccda89b..20b45166343 100644 --- a/contractcourt/htlc_success_resolver.go +++ b/contractcourt/htlc_success_resolver.go @@ -10,8 +10,6 @@ import ( "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" - "github.com/davecgh/go-spew/spew" - "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/graph/db/models" @@ -116,139 +114,60 @@ func (h *htlcSuccessResolver) ResolverKey() []byte { // anymore. Every HTLC has already passed through the incoming contest resolver // and in there the invoice was already marked as settled. // -// TODO(roasbeef): create multi to batch -// // NOTE: Part of the ContractResolver interface. +// +// TODO(yy): refactor the interface method to return an error only. func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) { - // If we're already resolved, then we can exit early. - if h.resolved { - return nil, nil - } - - // If we don't have a success transaction, then this means that this is - // an output on the remote party's commitment transaction. - if h.isRemoteCommitOutput() { - return h.resolveRemoteCommitOutput() - } - - // Otherwise this an output on our own commitment, and we must start by - // broadcasting the second-level success transaction. - secondLevelOutpoint, err := h.broadcastSuccessTx() - if err != nil { - return nil, err - } - - // To wrap this up, we'll wait until the second-level transaction has - // been spent, then fully resolve the contract. - return nil, h.resolveSuccessTxOutput(*secondLevelOutpoint) -} - -// broadcastSuccessTx handles an HTLC output on our local commitment by -// broadcasting the second-level success transaction. It returns the ultimate -// outpoint of the second-level tx, that we must wait to be spent for the -// resolver to be fully resolved. -func (h *htlcSuccessResolver) broadcastSuccessTx() ( - *wire.OutPoint, error) { - - // If we have non-nil SignDetails, this means that have a 2nd level - // HTLC transaction that is signed using sighash SINGLE|ANYONECANPAY - // (the case for anchor type channels). In this case we can re-sign it - // and attach fees at will. We let the sweeper handle this job. We use - // the checkpointed outputIncubating field to determine if we already - // swept the HTLC output into the second level transaction. - if h.isZeroFeeOutput() { - return h.broadcastReSignedSuccessTx() - } + var err error - // Otherwise we'll publish the second-level transaction directly and - // offer the resolution to the nursery to handle. - log.Infof("%T(%x): broadcasting second-layer transition tx: %v", - h, h.htlc.RHash[:], spew.Sdump(h.htlcResolution.SignedSuccessTx)) - - // We'll now broadcast the second layer transaction so we can kick off - // the claiming process. - err := h.resolveLegacySuccessTx() - if err != nil { - return nil, err - } - - return &h.htlcResolution.ClaimOutpoint, nil -} + switch { + // If we're already resolved, then we can exit early. + case h.resolved: + h.log.Errorf("already resolved") -// broadcastReSignedSuccessTx handles the case where we have non-nil -// SignDetails, and offers the second level transaction to the Sweeper, that -// will re-sign it and attach fees at will. -func (h *htlcSuccessResolver) broadcastReSignedSuccessTx() (*wire.OutPoint, - error) { - - // Keep track of the tx spending the HTLC output on the commitment, as - // this will be the confirmed second-level tx we'll ultimately sweep. - var commitSpend *chainntnfs.SpendDetail - - // We will have to let the sweeper re-sign the success tx and wait for - // it to confirm, if we haven't already. - if !h.outputIncubating { - err := h.sweepSuccessTx() - if err != nil { - return nil, err - } + // If this is an output on the remote party's commitment transaction, + // use the direct-spend path to sweep the htlc. + case h.isRemoteCommitOutput(): + err = h.resolveRemoteCommitOutput() + // If this is an output on our commitment transaction using post-anchor + // channel type, it will be handled by the sweeper. + case h.isZeroFeeOutput(): err = h.resolveSuccessTx() - if err != nil { - return nil, err - } - } - - // This should be non-blocking as we will only attempt to sweep the - // output when the second level tx has already been confirmed. In other - // words, waitForSpend will return immediately. - commitSpend, err := waitForSpend( - &h.htlcResolution.SignedSuccessTx.TxIn[0].PreviousOutPoint, - h.htlcResolution.SignDetails.SignDesc.Output.PkScript, - h.broadcastHeight, h.Notifier, h.quit, - ) - if err != nil { - return nil, err - } - err = h.sweepSuccessTxOutput() - if err != nil { - return nil, err + // If this is an output on our own commitment using pre-anchor channel + // type, we will publish the success tx and offer the output to the + // nursery. + default: + err = h.resolveLegacySuccessTx() } - // Will return this outpoint, when this is spent the resolver is fully - // resolved. - op := &wire.OutPoint{ - Hash: *commitSpend.SpenderTxHash, - Index: commitSpend.SpenderInputIndex, - } - - return op, nil + return nil, err } // resolveRemoteCommitOutput handles sweeping an HTLC output on the remote // commitment with the preimage. In this case we can sweep the output directly, // and don't have to broadcast a second-level transaction. -func (h *htlcSuccessResolver) resolveRemoteCommitOutput() ( - ContractResolver, error) { - - err := h.sweepRemoteCommitOutput() - if err != nil { - return nil, err - } +func (h *htlcSuccessResolver) resolveRemoteCommitOutput() error { + h.log.Info("waiting for direct-preimage spend of the htlc to confirm") // Wait for the direct-preimage HTLC sweep tx to confirm. + // + // TODO(yy): use the result chan returned from `SweepInput`. sweepTxDetails, err := waitForSpend( &h.htlcResolution.ClaimOutpoint, h.htlcResolution.SweepSignDesc.Output.PkScript, h.broadcastHeight, h.Notifier, h.quit, ) if err != nil { - return nil, err + return err } + // TODO(yy): should also update the `RecoveredBalance` and + // `LimboBalance` like other paths? + // Checkpoint the resolver, and write the outcome to disk. - return nil, h.checkpointClaim(sweepTxDetails.SpenderTxHash) + return h.checkpointClaim(sweepTxDetails.SpenderTxHash) } // checkpointClaim checkpoints the success resolver with the reports it needs. @@ -316,6 +235,9 @@ func (h *htlcSuccessResolver) checkpointClaim(spendTx *chainhash.Hash) error { // // NOTE: Part of the ContractResolver interface. func (h *htlcSuccessResolver) Stop() { + h.log.Debugf("stopping...") + defer h.log.Debugf("stopped") + close(h.quit) } @@ -809,3 +731,47 @@ func (h *htlcSuccessResolver) resolveSuccessTxOutput(op wire.OutPoint) error { return h.checkpointClaim(spend.SpenderTxHash) } + +// Launch creates an input based on the details of the incoming htlc resolution +// and offers it to the sweeper. +func (h *htlcSuccessResolver) Launch() error { + if h.launched { + h.log.Tracef("already launched") + return nil + } + + h.log.Debugf("launching resolver...") + h.launched = true + + switch { + // If we're already resolved, then we can exit early. + case h.resolved: + h.log.Errorf("already resolved") + return nil + + // If this is an output on the remote party's commitment transaction, + // use the direct-spend path. + case h.isRemoteCommitOutput(): + return h.sweepRemoteCommitOutput() + + // If this is an anchor type channel, we now sweep either the + // second-level success tx or the output from the second-level success + // tx. + case h.isZeroFeeOutput(): + // If the second-level success tx has already been swept, we + // can go ahead and sweep its output. + if h.outputIncubating { + return h.sweepSuccessTxOutput() + } + + // Otherwise, sweep the second level tx. + return h.sweepSuccessTx() + + // If this is a legacy channel type, the output is handled by the + // nursery via the Resolve so we do nothing here. + // + // TODO(yy): handle the legacy output by offering it to the sweeper. + default: + return nil + } +} diff --git a/contractcourt/htlc_success_resolver_test.go b/contractcourt/htlc_success_resolver_test.go index 75c733638fe..f395d67215a 100644 --- a/contractcourt/htlc_success_resolver_test.go +++ b/contractcourt/htlc_success_resolver_test.go @@ -5,6 +5,7 @@ import ( "fmt" "reflect" "testing" + "time" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" @@ -20,6 +21,7 @@ import ( "github.com/lightningnetwork/lnd/lntest/mock" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwire" + "github.com/stretchr/testify/require" ) var testHtlcAmt = lnwire.MilliSatoshi(200000) @@ -39,6 +41,15 @@ type htlcResolverTestContext struct { t *testing.T } +func newHtlcResolverTestContextFromReader(t *testing.T, + newResolver func(htlc channeldb.HTLC, + cfg ResolverConfig) ContractResolver) *htlcResolverTestContext { + + ctx := newHtlcResolverTestContext(t, newResolver) + + return ctx +} + func newHtlcResolverTestContext(t *testing.T, newResolver func(htlc channeldb.HTLC, cfg ResolverConfig) ContractResolver) *htlcResolverTestContext { @@ -133,6 +144,7 @@ func newHtlcResolverTestContext(t *testing.T, func (i *htlcResolverTestContext) resolve() { // Start resolver. i.resolverResultChan = make(chan resolveResult, 1) + go func() { nextResolver, err := i.resolver.Resolve() i.resolverResultChan <- resolveResult{ @@ -192,6 +204,7 @@ func TestHtlcSuccessSingleStage(t *testing.T) { // sweeper. details := &chainntnfs.SpendDetail{ SpendingTx: sweepTx, + SpentOutPoint: &htlcOutpoint, SpenderTxHash: &sweepTxid, } ctx.notifier.SpendChan <- details @@ -215,8 +228,8 @@ func TestHtlcSuccessSingleStage(t *testing.T) { ) } -// TestSecondStageResolution tests successful sweep of a second stage htlc -// claim, going through the Nursery. +// TestHtlcSuccessSecondStageResolution tests successful sweep of a second +// stage htlc claim, going through the Nursery. func TestHtlcSuccessSecondStageResolution(t *testing.T) { commitOutpoint := wire.OutPoint{Index: 2} htlcOutpoint := wire.OutPoint{Index: 3} @@ -279,6 +292,7 @@ func TestHtlcSuccessSecondStageResolution(t *testing.T) { ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ SpendingTx: sweepTx, + SpentOutPoint: &htlcOutpoint, SpenderTxHash: &sweepHash, } @@ -302,6 +316,8 @@ func TestHtlcSuccessSecondStageResolution(t *testing.T) { // TestHtlcSuccessSecondStageResolutionSweeper test that a resolver with // non-nil SignDetails will offer the second-level transaction to the sweeper // for re-signing. +// +//nolint:ll func TestHtlcSuccessSecondStageResolutionSweeper(t *testing.T) { commitOutpoint := wire.OutPoint{Index: 2} htlcOutpoint := wire.OutPoint{Index: 3} @@ -399,7 +415,20 @@ func TestHtlcSuccessSecondStageResolutionSweeper(t *testing.T) { _ bool) error { resolver := ctx.resolver.(*htlcSuccessResolver) - inp := <-resolver.Sweeper.(*mockSweeper).sweptInputs + + var ( + inp input.Input + ok bool + ) + + select { + case inp, ok = <-resolver.Sweeper.(*mockSweeper).sweptInputs: + require.True(t, ok) + + case <-time.After(1 * time.Second): + t.Fatal("expected input to be swept") + } + op := inp.OutPoint() if op != commitOutpoint { return fmt.Errorf("outpoint %v swept, "+ @@ -412,6 +441,7 @@ func TestHtlcSuccessSecondStageResolutionSweeper(t *testing.T) { SpenderTxHash: &reSignedHash, SpenderInputIndex: 1, SpendingHeight: 10, + SpentOutPoint: &commitOutpoint, } return nil }, @@ -434,13 +464,37 @@ func TestHtlcSuccessSecondStageResolutionSweeper(t *testing.T) { SpenderTxHash: &reSignedHash, SpenderInputIndex: 1, SpendingHeight: 10, + SpentOutPoint: &commitOutpoint, } } // We expect it to sweep the second-level // transaction we notfied about above. resolver := ctx.resolver.(*htlcSuccessResolver) - inp := <-resolver.Sweeper.(*mockSweeper).sweptInputs + + // Mock `waitForSpend` to return the commit + // spend. + ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ + SpendingTx: reSignedSuccessTx, + SpenderTxHash: &reSignedHash, + SpenderInputIndex: 1, + SpendingHeight: 10, + SpentOutPoint: &commitOutpoint, + } + + var ( + inp input.Input + ok bool + ) + + select { + case inp, ok = <-resolver.Sweeper.(*mockSweeper).sweptInputs: + require.True(t, ok) + + case <-time.After(1 * time.Second): + t.Fatal("expected input to be swept") + } + op := inp.OutPoint() exp := wire.OutPoint{ Hash: reSignedHash, @@ -457,6 +511,7 @@ func TestHtlcSuccessSecondStageResolutionSweeper(t *testing.T) { SpendingTx: sweepTx, SpenderTxHash: &sweepHash, SpendingHeight: 14, + SpentOutPoint: &op, } return nil @@ -504,11 +559,14 @@ func testHtlcSuccess(t *testing.T, resolution lnwallet.IncomingHtlcResolution, // for the next portion of the test. ctx := newHtlcResolverTestContext(t, func(htlc channeldb.HTLC, cfg ResolverConfig) ContractResolver { - return &htlcSuccessResolver{ + r := &htlcSuccessResolver{ contractResolverKit: *newContractResolverKit(cfg), htlc: htlc, htlcResolution: resolution, } + r.initLogger("htlcSuccessResolver") + + return r }, ) @@ -606,7 +664,12 @@ func runFromCheckpoint(t *testing.T, ctx *htlcResolverTestContext, checkpointedState = append(checkpointedState, b.Bytes()) nextCheckpoint++ - checkpointChan <- struct{}{} + select { + case checkpointChan <- struct{}{}: + case <-time.After(1 * time.Second): + t.Fatal("checkpoint timeout") + } + return nil } @@ -617,6 +680,8 @@ func runFromCheckpoint(t *testing.T, ctx *htlcResolverTestContext, // preCheckpoint logic if needed. resumed := true for i, cp := range expectedCheckpoints { + t.Logf("Running checkpoint %d", i) + if cp.preCheckpoint != nil { if err := cp.preCheckpoint(ctx, resumed); err != nil { t.Fatalf("failure at stage %d: %v", i, err) @@ -625,15 +690,15 @@ func runFromCheckpoint(t *testing.T, ctx *htlcResolverTestContext, resumed = false // Wait for the resolver to have checkpointed its state. - <-checkpointChan + select { + case <-checkpointChan: + case <-time.After(1 * time.Second): + t.Fatalf("resolver did not checkpoint at stage %d", i) + } } // Wait for the resolver to fully complete. ctx.waitForResult() - if nextCheckpoint < len(expectedCheckpoints) { - t.Fatalf("not all checkpoints hit") - } - return checkpointedState } From 71aec7bd94d62ffb17acc4c7907442eca3291a5b Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 16 Jul 2024 09:05:47 +0800 Subject: [PATCH 43/92] contractcourt: add `Launch` method to htlc timeout resolver This commit breaks the `Resolve` into two parts - the first part is moved into a `Launch` method that handles sending sweep requests, and the second part remains in `Resolve` which handles waiting for the spend. Since we are using both utxo nursery and sweeper at the same time, to make sure this change doesn't break the existing behavior, we implement the `Launch` as following, - zero-fee htlc - handled by the sweeper - direct output from the remote commit - handled by the sweeper - legacy htlc - handled by the utxo nursery --- contractcourt/channel_arbitrator_test.go | 7 +- contractcourt/htlc_timeout_resolver.go | 162 ++++++------ contractcourt/htlc_timeout_resolver_test.go | 272 +++++++++++++------- 3 files changed, 254 insertions(+), 187 deletions(-) diff --git a/contractcourt/channel_arbitrator_test.go b/contractcourt/channel_arbitrator_test.go index a3bdf6ba3b9..d1ef2993d50 100644 --- a/contractcourt/channel_arbitrator_test.go +++ b/contractcourt/channel_arbitrator_test.go @@ -982,6 +982,7 @@ func TestChannelArbitratorLocalForceClosePendingHtlc(t *testing.T) { }, }, } + closeTxid := closeTx.TxHash() htlcOp := wire.OutPoint{ Hash: closeTx.TxHash(), @@ -1117,7 +1118,11 @@ func TestChannelArbitratorLocalForceClosePendingHtlc(t *testing.T) { // Notify resolver that the HTLC output of the commitment has been // spent. - oldNotifier.SpendChan <- &chainntnfs.SpendDetail{SpendingTx: closeTx} + oldNotifier.SpendChan <- &chainntnfs.SpendDetail{ + SpendingTx: closeTx, + SpentOutPoint: &wire.OutPoint{}, + SpenderTxHash: &closeTxid, + } // Finally, we should also receive a resolution message instructing the // switch to cancel back the HTLC. diff --git a/contractcourt/htlc_timeout_resolver.go b/contractcourt/htlc_timeout_resolver.go index 9904f37d3d7..eae52255f2b 100644 --- a/contractcourt/htlc_timeout_resolver.go +++ b/contractcourt/htlc_timeout_resolver.go @@ -425,40 +425,25 @@ func checkSizeAndIndex(witness wire.TxWitness, size, index int) bool { func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) { // If we're already resolved, then we can exit early. if h.resolved { + h.log.Errorf("already resolved") return nil, nil } - // Start by spending the HTLC output, either by broadcasting the - // second-level timeout transaction, or directly if this is the remote - // commitment. - commitSpend, err := h.spendHtlcOutput() - if err != nil { - return nil, err - } - - // If the spend reveals the pre-image, then we'll enter the clean up - // workflow to pass the pre-image back to the incoming link, add it to - // the witness cache, and exit. - if isPreimageSpend( - h.isTaproot(), commitSpend, - h.htlcResolution.SignedTimeoutTx != nil, - ) { - - log.Infof("%T(%v): HTLC has been swept with pre-image by "+ - "remote party during timeout flow! Adding pre-image to "+ - "witness cache", h, h.htlc.RHash[:], - h.htlcResolution.ClaimOutpoint) - - return nil, h.claimCleanUp(commitSpend) - } - - // Depending on whether this was a local or remote commit, we must - // handle the spending transaction accordingly. + // If this is an output on the remote party's commitment transaction, + // use the direct-spend path to sweep the htlc. if h.isRemoteCommitOutput() { return nil, h.resolveRemoteCommitOutput() } - return nil, h.resolveTimeoutTx() + // If this is a zero-fee HTLC, we now handle the spend from our + // commitment transaction. + if h.isZeroFeeOutput() { + return nil, h.resolveTimeoutTx() + } + + // If this is an output on our own commitment using pre-anchor channel + // type, we will let the utxo nursery handle it. + return nil, h.resolveSecondLevelTxLegacy() } // sweepTimeoutTx sends a second level timeout transaction to the sweeper. @@ -521,11 +506,16 @@ func (h *htlcTimeoutResolver) resolveSecondLevelTxLegacy() error { // The utxo nursery will take care of broadcasting the second-level // timeout tx and sweeping its output once it confirms. - return h.IncubateOutputs( + err := h.IncubateOutputs( h.ChanPoint, fn.Some(h.htlcResolution), fn.None[lnwallet.IncomingHtlcResolution](), h.broadcastHeight, h.incomingHTLCExpiryHeight, ) + if err != nil { + return err + } + + return h.resolveTimeoutTx() } // sweepDirectHtlcOutput sends the direct spend of the HTLC output to the @@ -581,53 +571,6 @@ func (h *htlcTimeoutResolver) sweepDirectHtlcOutput() error { return nil } -// spendHtlcOutput handles the initial spend of an HTLC output via the timeout -// clause. If this is our local commitment, the second-level timeout TX will be -// used to spend the output into the next stage. If this is the remote -// commitment, the output will be swept directly without the timeout -// transaction. -func (h *htlcTimeoutResolver) spendHtlcOutput() ( - *chainntnfs.SpendDetail, error) { - - switch { - // If we have non-nil SignDetails, this means that have a 2nd level - // HTLC transaction that is signed using sighash SINGLE|ANYONECANPAY - // (the case for anchor type channels). In this case we can re-sign it - // and attach fees at will. We let the sweeper handle this job. - case h.isZeroFeeOutput() && !h.outputIncubating: - if err := h.sweepTimeoutTx(); err != nil { - log.Errorf("Sending timeout tx to sweeper: %v", err) - - return nil, err - } - - // If this is a remote commitment there's no second level timeout txn, - // and we can just send this directly to the sweeper. - case h.isRemoteCommitOutput() && !h.outputIncubating: - if err := h.sweepDirectHtlcOutput(); err != nil { - log.Errorf("Sending direct spend to sweeper: %v", err) - - return nil, err - } - - // If we have a SignedTimeoutTx but no SignDetails, this is a local - // commitment for a non-anchor channel, so we'll send it to the utxo - // nursery. - case h.isLegacyOutput() && !h.outputIncubating: - if err := h.resolveSecondLevelTxLegacy(); err != nil { - log.Errorf("Sending timeout tx to nursery: %v", err) - - return nil, err - } - } - - // Now that we've handed off the HTLC to the nursery or sweeper, we'll - // watch for a spend of the output, and make our next move off of that. - // Depending on if this is our commitment, or the remote party's - // commitment, we'll be watching a different outpoint and script. - return h.watchHtlcSpend() -} - // watchHtlcSpend watches for a spend of the HTLC output. For neutrino backend, // it will check blocks for the confirmed spend. For btcd and bitcoind, it will // check both the mempool and the blocks. @@ -673,6 +616,9 @@ func (h *htlcTimeoutResolver) waitForConfirmedSpend(op *wire.OutPoint, // // NOTE: Part of the ContractResolver interface. func (h *htlcTimeoutResolver) Stop() { + h.log.Debugf("stopping...") + defer h.log.Debugf("stopped") + close(h.quit) } @@ -1018,15 +964,6 @@ func (h *htlcTimeoutResolver) isZeroFeeOutput() bool { h.htlcResolution.SignDetails != nil } -// isLegacyOutput returns a boolean indicating whether the htlc output is from -// a non-anchor-enabled channel. -func (h *htlcTimeoutResolver) isLegacyOutput() bool { - // If we have a SignedTimeoutTx but no SignDetails, this is a local - // commitment for a non-anchor channel. - return h.htlcResolution.SignedTimeoutTx != nil && - h.htlcResolution.SignDetails == nil -} - // waitHtlcSpendAndCheckPreimage waits for the htlc output to be spent and // checks whether the spending reveals the preimage. If the preimage is found, // it will be added to the preimage beacon to settle the incoming link, and a @@ -1296,9 +1233,11 @@ func (h *htlcTimeoutResolver) resolveTimeoutTx() error { h.log.Infof("2nd-level HTLC timeout tx=%v confirmed", spenderTxid) // Start the process to sweep the output from the timeout tx. - err = h.sweepTimeoutTxOutput() - if err != nil { - return err + if h.isZeroFeeOutput() { + err = h.sweepTimeoutTxOutput() + if err != nil { + return err + } } // Create a checkpoint since the timeout tx is confirmed and the sweep @@ -1332,3 +1271,52 @@ func (h *htlcTimeoutResolver) resolveTimeoutTxOutput(op wire.OutPoint) error { return h.checkpointClaim(spend) } + +// Launch creates an input based on the details of the outgoing htlc resolution +// and offers it to the sweeper. +func (h *htlcTimeoutResolver) Launch() error { + if h.launched { + h.log.Tracef("already launched") + return nil + } + + h.log.Debugf("launching resolver...") + h.launched = true + + switch { + // If we're already resolved, then we can exit early. + case h.resolved: + h.log.Errorf("already resolved") + return nil + + // If this is an output on the remote party's commitment transaction, + // use the direct timeout spend path. + // + // NOTE: When the outputIncubating is false, it means that the output + // has been offered to the utxo nursery as starting in 0.18.4, we + // stopped marking this flag for direct timeout spends (#9062). In that + // case, we will do nothing and let the utxo nursery handle it. + case h.isRemoteCommitOutput() && !h.outputIncubating: + return h.sweepDirectHtlcOutput() + + // If this is an anchor type channel, we now sweep either the + // second-level timeout tx or the output from the second-level timeout + // tx. + case h.isZeroFeeOutput(): + // If the second-level timeout tx has already been swept, we + // can go ahead and sweep its output. + if h.outputIncubating { + return h.sweepTimeoutTxOutput() + } + + // Otherwise, sweep the second level tx. + return h.sweepTimeoutTx() + + // If this is an output on our own commitment using pre-anchor channel + // type, we will let the utxo nursery handle it via Resolve. + // + // TODO(yy): handle the legacy output by offering it to the sweeper. + default: + return nil + } +} diff --git a/contractcourt/htlc_timeout_resolver_test.go b/contractcourt/htlc_timeout_resolver_test.go index 17341f0c20b..89f334aa83b 100644 --- a/contractcourt/htlc_timeout_resolver_test.go +++ b/contractcourt/htlc_timeout_resolver_test.go @@ -40,7 +40,7 @@ type mockWitnessBeacon struct { func newMockWitnessBeacon() *mockWitnessBeacon { return &mockWitnessBeacon{ preImageUpdates: make(chan lntypes.Preimage, 1), - newPreimages: make(chan []lntypes.Preimage), + newPreimages: make(chan []lntypes.Preimage, 1), lookupPreimage: make(map[lntypes.Hash]lntypes.Preimage), } } @@ -280,7 +280,7 @@ func testHtlcTimeoutResolver(t *testing.T, testCase htlcTimeoutTestCase) { notifier := &mock.ChainNotifier{ EpochChan: make(chan *chainntnfs.BlockEpoch), - SpendChan: make(chan *chainntnfs.SpendDetail), + SpendChan: make(chan *chainntnfs.SpendDetail, 1), ConfChan: make(chan *chainntnfs.TxConfirmation), } @@ -321,6 +321,7 @@ func testHtlcTimeoutResolver(t *testing.T, testCase htlcTimeoutTestCase) { return nil }, + HtlcNotifier: &mockHTLCNotifier{}, }, PutResolverReport: func(_ kvdb.RwTx, _ *channeldb.ResolverReport) error { @@ -356,6 +357,7 @@ func testHtlcTimeoutResolver(t *testing.T, testCase htlcTimeoutTestCase) { Amt: testHtlcAmt, }, } + resolver.initLogger("timeoutResolver") var reports []*channeldb.ResolverReport @@ -390,7 +392,12 @@ func testHtlcTimeoutResolver(t *testing.T, testCase htlcTimeoutTestCase) { go func() { defer wg.Done() - _, err := resolver.Resolve() + err := resolver.Launch() + if err != nil { + resolveErr <- err + } + + _, err = resolver.Resolve() if err != nil { resolveErr <- err } @@ -406,8 +413,7 @@ func testHtlcTimeoutResolver(t *testing.T, testCase htlcTimeoutTestCase) { sweepChan = mockSweeper.sweptInputs } - // The output should be offered to either the sweeper or - // the nursery. + // The output should be offered to either the sweeper or the nursery. select { case <-incubateChan: case <-sweepChan: @@ -431,6 +437,7 @@ func testHtlcTimeoutResolver(t *testing.T, testCase htlcTimeoutTestCase) { case notifier.SpendChan <- &chainntnfs.SpendDetail{ SpendingTx: spendingTx, SpenderTxHash: &spendTxHash, + SpentOutPoint: &testChanPoint2, }: case <-time.After(time.Second * 5): t.Fatalf("failed to request spend ntfn") @@ -487,6 +494,7 @@ func testHtlcTimeoutResolver(t *testing.T, testCase htlcTimeoutTestCase) { case notifier.SpendChan <- &chainntnfs.SpendDetail{ SpendingTx: spendingTx, SpenderTxHash: &spendTxHash, + SpentOutPoint: &testChanPoint2, }: case <-time.After(time.Second * 5): t.Fatalf("failed to request spend ntfn") @@ -549,6 +557,8 @@ func TestHtlcTimeoutResolver(t *testing.T) { // TestHtlcTimeoutSingleStage tests a remote commitment confirming, and the // local node sweeping the HTLC output directly after timeout. +// +//nolint:ll func TestHtlcTimeoutSingleStage(t *testing.T) { commitOutpoint := wire.OutPoint{Index: 3} @@ -573,6 +583,12 @@ func TestHtlcTimeoutSingleStage(t *testing.T) { SpendTxID: &sweepTxid, } + sweepSpend := &chainntnfs.SpendDetail{ + SpendingTx: sweepTx, + SpentOutPoint: &commitOutpoint, + SpenderTxHash: &sweepTxid, + } + checkpoints := []checkpoint{ { // We send a confirmation the sweep tx from published @@ -582,9 +598,10 @@ func TestHtlcTimeoutSingleStage(t *testing.T) { // The nursery will create and publish a sweep // tx. - ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ - SpendingTx: sweepTx, - SpenderTxHash: &sweepTxid, + select { + case ctx.notifier.SpendChan <- sweepSpend: + case <-time.After(time.Second * 5): + t.Fatalf("failed to send spend ntfn") } // The resolver should deliver a failure @@ -620,7 +637,9 @@ func TestHtlcTimeoutSingleStage(t *testing.T) { // TestHtlcTimeoutSecondStage tests a local commitment being confirmed, and the // local node claiming the HTLC output using the second-level timeout tx. -func TestHtlcTimeoutSecondStage(t *testing.T) { +// +//nolint:ll +func TestHtlcTimeoutSecondStagex(t *testing.T) { commitOutpoint := wire.OutPoint{Index: 2} htlcOutpoint := wire.OutPoint{Index: 3} @@ -678,23 +697,57 @@ func TestHtlcTimeoutSecondStage(t *testing.T) { SpendTxID: &sweepHash, } + timeoutSpend := &chainntnfs.SpendDetail{ + SpendingTx: timeoutTx, + SpentOutPoint: &commitOutpoint, + SpenderTxHash: &timeoutTxid, + } + + sweepSpend := &chainntnfs.SpendDetail{ + SpendingTx: sweepTx, + SpentOutPoint: &htlcOutpoint, + SpenderTxHash: &sweepHash, + } + checkpoints := []checkpoint{ { + preCheckpoint: func(ctx *htlcResolverTestContext, + _ bool) error { + + // Deliver spend of timeout tx. + ctx.notifier.SpendChan <- timeoutSpend + + return nil + }, + // Output should be handed off to the nursery. incubating: true, + reports: []*channeldb.ResolverReport{ + firstStage, + }, }, { // We send a confirmation for our sweep tx to indicate // that our sweep succeeded. preCheckpoint: func(ctx *htlcResolverTestContext, - _ bool) error { + resumed bool) error { - // The nursery will publish the timeout tx. - ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ - SpendingTx: timeoutTx, - SpenderTxHash: &timeoutTxid, + // When it's reloaded from disk, we need to + // re-send the notification to mock the first + // `watchHtlcSpend`. + if resumed { + // Deliver spend of timeout tx. + ctx.notifier.SpendChan <- timeoutSpend + + // Deliver spend of timeout tx output. + ctx.notifier.SpendChan <- sweepSpend + + return nil } + // Deliver spend of timeout tx output. + ctx.notifier.SpendChan <- sweepSpend + // The resolver should deliver a failure // resolution message (indicating we // successfully timed out the HTLC). @@ -707,12 +760,6 @@ func TestHtlcTimeoutSecondStage(t *testing.T) { t.Fatalf("resolution not sent") } - // Deliver spend of timeout tx. - ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ - SpendingTx: sweepTx, - SpenderTxHash: &sweepHash, - } - return nil }, @@ -722,7 +769,7 @@ func TestHtlcTimeoutSecondStage(t *testing.T) { incubating: true, resolved: true, reports: []*channeldb.ResolverReport{ - firstStage, secondState, + secondState, }, }, } @@ -796,10 +843,6 @@ func TestHtlcTimeoutSingleStageRemoteSpend(t *testing.T) { } checkpoints := []checkpoint{ - { - // Output should be handed off to the nursery. - incubating: true, - }, { // We send a spend notification for a remote spend with // the preimage. @@ -812,6 +855,7 @@ func TestHtlcTimeoutSingleStageRemoteSpend(t *testing.T) { // the preimage. ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ SpendingTx: spendTx, + SpentOutPoint: &commitOutpoint, SpenderTxHash: &spendTxHash, } @@ -847,7 +891,7 @@ func TestHtlcTimeoutSingleStageRemoteSpend(t *testing.T) { // After the success tx has confirmed, we expect the // checkpoint to be resolved, and with the above // report. - incubating: true, + incubating: false, resolved: true, reports: []*channeldb.ResolverReport{ claim, @@ -914,6 +958,7 @@ func TestHtlcTimeoutSecondStageRemoteSpend(t *testing.T) { ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ SpendingTx: remoteSuccessTx, + SpentOutPoint: &commitOutpoint, SpenderTxHash: &successTxid, } @@ -967,20 +1012,15 @@ func TestHtlcTimeoutSecondStageRemoteSpend(t *testing.T) { // TestHtlcTimeoutSecondStageSweeper tests that for anchor channels, when a // local commitment confirms, the timeout tx is handed to the sweeper to claim // the HTLC output. +// +//nolint:ll func TestHtlcTimeoutSecondStageSweeper(t *testing.T) { - commitOutpoint := wire.OutPoint{Index: 2} htlcOutpoint := wire.OutPoint{Index: 3} - sweepTx := &wire.MsgTx{ - TxIn: []*wire.TxIn{{}}, - TxOut: []*wire.TxOut{{}}, - } - sweepHash := sweepTx.TxHash() - timeoutTx := &wire.MsgTx{ TxIn: []*wire.TxIn{ { - PreviousOutPoint: commitOutpoint, + PreviousOutPoint: htlcOutpoint, }, }, TxOut: []*wire.TxOut{ @@ -1027,11 +1067,16 @@ func TestHtlcTimeoutSecondStageSweeper(t *testing.T) { }, } reSignedHash := reSignedTimeoutTx.TxHash() - reSignedOutPoint := wire.OutPoint{ + + timeoutTxOutpoint := wire.OutPoint{ Hash: reSignedHash, Index: 1, } + // Make a copy so `isPreimageSpend` can easily pass. + sweepTx := reSignedTimeoutTx.Copy() + sweepHash := sweepTx.TxHash() + // twoStageResolution is a resolution for a htlc on the local // party's commitment, where the timeout tx can be re-signed. twoStageResolution := lnwallet.OutgoingHtlcResolution{ @@ -1045,7 +1090,7 @@ func TestHtlcTimeoutSecondStageSweeper(t *testing.T) { } firstStage := &channeldb.ResolverReport{ - OutPoint: commitOutpoint, + OutPoint: htlcOutpoint, Amount: testHtlcAmt.ToSatoshis(), ResolverType: channeldb.ResolverTypeOutgoingHtlc, ResolverOutcome: channeldb.ResolverOutcomeFirstStage, @@ -1053,12 +1098,45 @@ func TestHtlcTimeoutSecondStageSweeper(t *testing.T) { } secondState := &channeldb.ResolverReport{ - OutPoint: reSignedOutPoint, + OutPoint: timeoutTxOutpoint, Amount: btcutil.Amount(testSignDesc.Output.Value), ResolverType: channeldb.ResolverTypeOutgoingHtlc, ResolverOutcome: channeldb.ResolverOutcomeTimeout, SpendTxID: &sweepHash, } + // mockTimeoutTxSpend is a helper closure to mock `waitForSpend` to + // return the commit spend in `sweepTimeoutTxOutput`. + mockTimeoutTxSpend := func(ctx *htlcResolverTestContext) { + select { + case ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ + SpendingTx: reSignedTimeoutTx, + SpenderInputIndex: 1, + SpenderTxHash: &reSignedHash, + SpendingHeight: 10, + SpentOutPoint: &htlcOutpoint, + }: + + case <-time.After(time.Second * 1): + t.Fatalf("spend not sent") + } + } + + // mockSweepTxSpend is a helper closure to mock `waitForSpend` to + // return the commit spend in `sweepTimeoutTxOutput`. + mockSweepTxSpend := func(ctx *htlcResolverTestContext) { + select { + case ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ + SpendingTx: sweepTx, + SpenderInputIndex: 1, + SpenderTxHash: &sweepHash, + SpendingHeight: 10, + SpentOutPoint: &timeoutTxOutpoint, + }: + + case <-time.After(time.Second * 1): + t.Fatalf("spend not sent") + } + } checkpoints := []checkpoint{ { @@ -1067,28 +1145,40 @@ func TestHtlcTimeoutSecondStageSweeper(t *testing.T) { _ bool) error { resolver := ctx.resolver.(*htlcTimeoutResolver) - inp := <-resolver.Sweeper.(*mockSweeper).sweptInputs + + var ( + inp input.Input + ok bool + ) + + select { + case inp, ok = <-resolver.Sweeper.(*mockSweeper).sweptInputs: + require.True(t, ok) + + case <-time.After(1 * time.Second): + t.Fatal("expected input to be swept") + } + op := inp.OutPoint() - if op != commitOutpoint { + if op != htlcOutpoint { return fmt.Errorf("outpoint %v swept, "+ - "expected %v", op, - commitOutpoint) + "expected %v", op, htlcOutpoint) } - // Emulat the sweeper spending using the - // re-signed timeout tx. - ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ - SpendingTx: reSignedTimeoutTx, - SpenderInputIndex: 1, - SpenderTxHash: &reSignedHash, - SpendingHeight: 10, - } + // Mock `waitForSpend` twice, called in, + // - `resolveReSignedTimeoutTx` + // - `sweepTimeoutTxOutput`. + mockTimeoutTxSpend(ctx) + mockTimeoutTxSpend(ctx) return nil }, // incubating=true is used to signal that the // second-level transaction was confirmed. incubating: true, + reports: []*channeldb.ResolverReport{ + firstStage, + }, }, { // We send a confirmation for our sweep tx to indicate @@ -1096,18 +1186,18 @@ func TestHtlcTimeoutSecondStageSweeper(t *testing.T) { preCheckpoint: func(ctx *htlcResolverTestContext, resumed bool) error { - // If we are resuming from a checkpoint, we - // expect the resolver to re-subscribe to a - // spend, hence we must resend it. + // Mock `waitForSpend` to return the commit + // spend. if resumed { - ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ - SpendingTx: reSignedTimeoutTx, - SpenderInputIndex: 1, - SpenderTxHash: &reSignedHash, - SpendingHeight: 10, - } + mockTimeoutTxSpend(ctx) + mockTimeoutTxSpend(ctx) + mockSweepTxSpend(ctx) + + return nil } + mockSweepTxSpend(ctx) + // The resolver should deliver a failure // resolution message (indicating we // successfully timed out the HTLC). @@ -1123,7 +1213,20 @@ func TestHtlcTimeoutSecondStageSweeper(t *testing.T) { // The timeout tx output should now be given to // the sweeper. resolver := ctx.resolver.(*htlcTimeoutResolver) - inp := <-resolver.Sweeper.(*mockSweeper).sweptInputs + + var ( + inp input.Input + ok bool + ) + + select { + case inp, ok = <-resolver.Sweeper.(*mockSweeper).sweptInputs: + require.True(t, ok) + + case <-time.After(1 * time.Second): + t.Fatal("expected input to be swept") + } + op := inp.OutPoint() exp := wire.OutPoint{ Hash: reSignedHash, @@ -1133,14 +1236,6 @@ func TestHtlcTimeoutSecondStageSweeper(t *testing.T) { return fmt.Errorf("wrong outpoint swept") } - // Notify about the spend, which should resolve - // the resolver. - ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ - SpendingTx: sweepTx, - SpenderTxHash: &sweepHash, - SpendingHeight: 14, - } - return nil }, @@ -1150,7 +1245,6 @@ func TestHtlcTimeoutSecondStageSweeper(t *testing.T) { incubating: true, resolved: true, reports: []*channeldb.ResolverReport{ - firstStage, secondState, }, }, @@ -1231,33 +1325,6 @@ func TestHtlcTimeoutSecondStageSweeperRemoteSpend(t *testing.T) { } checkpoints := []checkpoint{ - { - // The output should be given to the sweeper. - preCheckpoint: func(ctx *htlcResolverTestContext, - _ bool) error { - - resolver := ctx.resolver.(*htlcTimeoutResolver) - inp := <-resolver.Sweeper.(*mockSweeper).sweptInputs - op := inp.OutPoint() - if op != commitOutpoint { - return fmt.Errorf("outpoint %v swept, "+ - "expected %v", op, - commitOutpoint) - } - - // Emulate the remote sweeping the output with the preimage. - // re-signed timeout tx. - ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ - SpendingTx: spendTx, - SpenderTxHash: &spendTxHash, - } - - return nil - }, - // incubating=true is used to signal that the - // second-level transaction was confirmed. - incubating: true, - }, { // We send a confirmation for our sweep tx to indicate // that our sweep succeeded. @@ -1272,6 +1339,7 @@ func TestHtlcTimeoutSecondStageSweeperRemoteSpend(t *testing.T) { ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ SpendingTx: spendTx, SpenderTxHash: &spendTxHash, + SpentOutPoint: &commitOutpoint, } } @@ -1309,7 +1377,7 @@ func TestHtlcTimeoutSecondStageSweeperRemoteSpend(t *testing.T) { // After the sweep has confirmed, we expect the // checkpoint to be resolved, and with the above // reports. - incubating: true, + incubating: false, resolved: true, reports: []*channeldb.ResolverReport{ claim, @@ -1334,21 +1402,26 @@ func testHtlcTimeout(t *testing.T, resolution lnwallet.OutgoingHtlcResolution, // for the next portion of the test. ctx := newHtlcResolverTestContext(t, func(htlc channeldb.HTLC, cfg ResolverConfig) ContractResolver { - return &htlcTimeoutResolver{ + r := &htlcTimeoutResolver{ contractResolverKit: *newContractResolverKit(cfg), htlc: htlc, htlcResolution: resolution, } + r.initLogger("htlcTimeoutResolver") + + return r }, ) checkpointedState := runFromCheckpoint(t, ctx, checkpoints) + t.Log("Running resolver to completion after restart") + // Now, from every checkpoint created, we re-create the resolver, and // run the test from that checkpoint. for i := range checkpointedState { cp := bytes.NewReader(checkpointedState[i]) - ctx := newHtlcResolverTestContext(t, + ctx := newHtlcResolverTestContextFromReader(t, func(htlc channeldb.HTLC, cfg ResolverConfig) ContractResolver { resolver, err := newTimeoutResolverFromReader(cp, cfg) if err != nil { @@ -1356,7 +1429,8 @@ func testHtlcTimeout(t *testing.T, resolution lnwallet.OutgoingHtlcResolution, } resolver.Supplement(htlc) - resolver.htlcResolution = resolution + resolver.initLogger("htlcTimeoutResolver") + return resolver }, ) From 025d787fd2ab6f3838405bdc342065bd6e2afec8 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sun, 17 Nov 2024 10:48:16 +0800 Subject: [PATCH 44/92] invoices: exit early when the subscriber chan is nil When calling `NotifyExitHopHtlc` it is allowed to pass a chan to subscribe to the HTLC's resolution when it's settled. However, this method will also return immediately if there's already a resolution, which means it behaves like a notifier and a getter. If the caller decides to only use the getter to do a non-blocking lookup, it can pass a nil subscriber chan to bypass the notification. --- contractcourt/mock_registry_test.go | 5 +++++ invoices/invoiceregistry.go | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/contractcourt/mock_registry_test.go b/contractcourt/mock_registry_test.go index 5bba11afcb2..0530ab51dd4 100644 --- a/contractcourt/mock_registry_test.go +++ b/contractcourt/mock_registry_test.go @@ -29,6 +29,11 @@ func (r *mockRegistry) NotifyExitHopHtlc(payHash lntypes.Hash, wireCustomRecords lnwire.CustomRecords, payload invoices.Payload) (invoices.HtlcResolution, error) { + // Exit early if the notification channel is nil. + if hodlChan == nil { + return r.notifyResolution, r.notifyErr + } + r.notifyChan <- notifyExitHopData{ hodlChan: hodlChan, payHash: payHash, diff --git a/invoices/invoiceregistry.go b/invoices/invoiceregistry.go index f5a6c6a95fc..cc76d5aefa5 100644 --- a/invoices/invoiceregistry.go +++ b/invoices/invoiceregistry.go @@ -1275,7 +1275,11 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked( invoiceToExpire = makeInvoiceExpiry(ctx.hash, invoice) } - i.hodlSubscribe(hodlChan, ctx.circuitKey) + // Subscribe to the resolution if the caller specified a + // notification channel. + if hodlChan != nil { + i.hodlSubscribe(hodlChan, ctx.circuitKey) + } default: panic("unknown action") From ef98c52d10b2c3060947e245510d80ddaa402420 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sun, 17 Nov 2024 10:47:23 +0800 Subject: [PATCH 45/92] contractcourt: add `Launch` method to incoming contest resolver A minor refactor is done to support implementing `Launch`. --- .../htlc_incoming_contest_resolver.go | 259 +++++++++++++----- .../htlc_incoming_contest_resolver_test.go | 16 +- 2 files changed, 210 insertions(+), 65 deletions(-) diff --git a/contractcourt/htlc_incoming_contest_resolver.go b/contractcourt/htlc_incoming_contest_resolver.go index 0714fb13018..20904545746 100644 --- a/contractcourt/htlc_incoming_contest_resolver.go +++ b/contractcourt/htlc_incoming_contest_resolver.go @@ -78,6 +78,37 @@ func (h *htlcIncomingContestResolver) processFinalHtlcFail() error { return nil } +// Launch will call the inner resolver's launch method if the preimage can be +// found, otherwise it's a no-op. +func (h *htlcIncomingContestResolver) Launch() error { + // NOTE: we don't mark this resolver as launched as the inner resolver + // will set it when it's launched. + if h.launched { + h.log.Tracef("already launched") + return nil + } + + h.log.Debugf("launching contest resolver...") + + // Query the preimage and apply it if we already know it. + applied, err := h.findAndapplyPreimage() + if err != nil { + return err + } + + // No preimage found, leave it to be handled by the resolver. + if !applied { + return nil + } + + h.log.Debugf("found preimage for htlc=%x, transforming into success "+ + "resolver and launching it", h.htlc.RHash) + + // Once we've applied the preimage, we'll launch the inner resolver to + // attempt to claim the HTLC. + return h.htlcSuccessResolver.Launch() +} + // Resolve attempts to resolve this contract. As we don't yet know of the // preimage for the contract, we'll wait for one of two things to happen: // @@ -94,6 +125,7 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) { // If we're already full resolved, then we don't have anything further // to do. if h.resolved { + h.log.Errorf("already resolved") return nil, nil } @@ -101,8 +133,7 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) { // now. payload, nextHopOnionBlob, err := h.decodePayload() if err != nil { - log.Debugf("ChannelArbitrator(%v): cannot decode payload of "+ - "htlc %v", h.ChanPoint, h.HtlcPoint()) + h.log.Debugf("cannot decode payload of htlc %v", h.HtlcPoint()) // If we've locked in an htlc with an invalid payload on our // commitment tx, we don't need to resolve it. The other party @@ -177,65 +208,6 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) { return nil, h.Checkpoint(h, report) } - // applyPreimage is a helper function that will populate our internal - // resolver with the preimage we learn of. This should be called once - // the preimage is revealed so the inner resolver can properly complete - // its duties. The error return value indicates whether the preimage - // was properly applied. - applyPreimage := func(preimage lntypes.Preimage) error { - // Sanity check to see if this preimage matches our htlc. At - // this point it should never happen that it does not match. - if !preimage.Matches(h.htlc.RHash) { - return errors.New("preimage does not match hash") - } - - // Update htlcResolution with the matching preimage. - h.htlcResolution.Preimage = preimage - - log.Infof("%T(%v): applied preimage=%v", h, - h.htlcResolution.ClaimOutpoint, preimage) - - isSecondLevel := h.htlcResolution.SignedSuccessTx != nil - - // If we didn't have to go to the second level to claim (this - // is the remote commitment transaction), then we don't need to - // modify our canned witness. - if !isSecondLevel { - return nil - } - - isTaproot := txscript.IsPayToTaproot( - h.htlcResolution.SignedSuccessTx.TxOut[0].PkScript, - ) - - // If this is our commitment transaction, then we'll need to - // populate the witness for the second-level HTLC transaction. - switch { - // For taproot channels, the witness for sweeping with success - // looks like: - // - - // - // - // So we'll insert it at the 3rd index of the witness. - case isTaproot: - //nolint:ll - h.htlcResolution.SignedSuccessTx.TxIn[0].Witness[2] = preimage[:] - - // Within the witness for the success transaction, the - // preimage is the 4th element as it looks like: - // - // * <0> - // - // We'll populate it within the witness, as since this - // was a "contest" resolver, we didn't yet know of the - // preimage. - case !isTaproot: - h.htlcResolution.SignedSuccessTx.TxIn[0].Witness[3] = preimage[:] - } - - return nil - } - // Define a closure to process htlc resolutions either directly or // triggered by future notifications. processHtlcResolution := func(e invoices.HtlcResolution) ( @@ -247,7 +219,7 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) { // If the htlc resolution was a settle, apply the // preimage and return a success resolver. case *invoices.HtlcSettleResolution: - err := applyPreimage(resolution.Preimage) + err := h.applyPreimage(resolution.Preimage) if err != nil { return nil, err } @@ -313,6 +285,9 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) { return nil, err } + h.log.Debugf("received resolution from registry: %v", + resolution) + defer func() { h.Registry.HodlUnsubscribeAll(hodlQueue.ChanIn()) @@ -370,7 +345,9 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) { // However, we don't know how to ourselves, so we'll // return our inner resolver which has the knowledge to // do so. - if err := applyPreimage(preimage); err != nil { + h.log.Debugf("Found preimage for htlc=%x", h.htlc.RHash) + + if err := h.applyPreimage(preimage); err != nil { return nil, err } @@ -389,7 +366,10 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) { continue } - if err := applyPreimage(preimage); err != nil { + h.log.Debugf("Received preimage for htlc=%x", + h.htlc.RHash) + + if err := h.applyPreimage(preimage); err != nil { return nil, err } @@ -436,6 +416,76 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) { } } +// applyPreimage is a helper function that will populate our internal resolver +// with the preimage we learn of. This should be called once the preimage is +// revealed so the inner resolver can properly complete its duties. The error +// return value indicates whether the preimage was properly applied. +func (h *htlcIncomingContestResolver) applyPreimage( + preimage lntypes.Preimage) error { + + // Sanity check to see if this preimage matches our htlc. At this point + // it should never happen that it does not match. + if !preimage.Matches(h.htlc.RHash) { + return errors.New("preimage does not match hash") + } + + // We may already have the preimage since both the `Launch` and + // `Resolve` methods will look for it. + if h.htlcResolution.Preimage != lntypes.ZeroHash { + h.log.Debugf("already applied preimage for htlc=%x", + h.htlc.RHash) + + return nil + } + + // Update htlcResolution with the matching preimage. + h.htlcResolution.Preimage = preimage + + log.Infof("%T(%v): applied preimage=%v", h, + h.htlcResolution.ClaimOutpoint, preimage) + + isSecondLevel := h.htlcResolution.SignedSuccessTx != nil + + // If we didn't have to go to the second level to claim (this + // is the remote commitment transaction), then we don't need to + // modify our canned witness. + if !isSecondLevel { + return nil + } + + isTaproot := txscript.IsPayToTaproot( + h.htlcResolution.SignedSuccessTx.TxOut[0].PkScript, + ) + + // If this is our commitment transaction, then we'll need to + // populate the witness for the second-level HTLC transaction. + switch { + // For taproot channels, the witness for sweeping with success + // looks like: + // - + // + // + // So we'll insert it at the 3rd index of the witness. + case isTaproot: + //nolint:ll + h.htlcResolution.SignedSuccessTx.TxIn[0].Witness[2] = preimage[:] + + // Within the witness for the success transaction, the + // preimage is the 4th element as it looks like: + // + // * <0> + // + // We'll populate it within the witness, as since this + // was a "contest" resolver, we didn't yet know of the + // preimage. + case !isTaproot: + //nolint:ll + h.htlcResolution.SignedSuccessTx.TxIn[0].Witness[3] = preimage[:] + } + + return nil +} + // report returns a report on the resolution state of the contract. func (h *htlcIncomingContestResolver) report() *ContractReport { // No locking needed as these values are read-only. @@ -462,6 +512,8 @@ func (h *htlcIncomingContestResolver) report() *ContractReport { // // NOTE: Part of the ContractResolver interface. func (h *htlcIncomingContestResolver) Stop() { + h.log.Debugf("stopping...") + defer h.log.Debugf("stopped") close(h.quit) } @@ -561,3 +613,82 @@ func (h *htlcIncomingContestResolver) decodePayload() (*hop.Payload, // A compile time assertion to ensure htlcIncomingContestResolver meets the // ContractResolver interface. var _ htlcContractResolver = (*htlcIncomingContestResolver)(nil) + +// findAndapplyPreimage performs a non-blocking read to find the preimage for +// the incoming HTLC. If found, it will be applied to the resolver. This method +// is used for the resolver to decide whether it wants to transform into a +// success resolver during launching. +// +// 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) { + // Query to see if we already know the preimage. + preimage, ok := h.PreimageDB.LookupPreimage(h.htlc.RHash) + + // If the preimage is known, we'll apply it. + if ok { + if err := h.applyPreimage(preimage); err != nil { + return false, err + } + + // Successfully applied the preimage, we can now return. + return true, nil + } + + // First try to parse the payload. + payload, _, err := h.decodePayload() + if err != nil { + h.log.Errorf("Cannot decode payload of htlc %v", h.HtlcPoint()) + + // If we cannot decode the payload, we will return a nil error + // and let it to be handled in `Resolve`. + return false, nil + } + + // Exit early if this is not the exit hop, which means we are not the + // payment receiver and don't have preimage. + if payload.FwdInfo.NextHop != hop.Exit { + return false, nil + } + + // Notify registry that we are potentially resolving as an exit hop + // on-chain. If this HTLC indeed pays to an existing invoice, the + // invoice registry will tell us what to do with the HTLC. This is + // identical to HTLC resolution in the link. + circuitKey := models.CircuitKey{ + ChanID: h.ShortChanID, + HtlcID: h.htlc.HtlcIndex, + } + + // Try get the resolution - if it doesn't give us a resolution + // 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. + // + // 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, + circuitKey, nil, nil, payload, + ) + if err != nil { + return false, err + } + + res, ok := resolution.(*invoices.HtlcSettleResolution) + + // Exit early if it's not a settle resolution. + if !ok { + return false, nil + } + + // Otherwise we have a settle resolution, apply the preimage. + err = h.applyPreimage(res.Preimage) + if err != nil { + return false, err + } + + return true, nil +} diff --git a/contractcourt/htlc_incoming_contest_resolver_test.go b/contractcourt/htlc_incoming_contest_resolver_test.go index e8b8eac0c96..f17190e96e8 100644 --- a/contractcourt/htlc_incoming_contest_resolver_test.go +++ b/contractcourt/htlc_incoming_contest_resolver_test.go @@ -5,11 +5,13 @@ import ( "io" "testing" + "github.com/btcsuite/btcd/wire" sphinx "github.com/lightningnetwork/lightning-onion" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/graph/db/models" "github.com/lightningnetwork/lnd/htlcswitch/hop" + "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/invoices" "github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/lnmock" @@ -356,6 +358,7 @@ func newIncomingResolverTestContext(t *testing.T, isExit bool) *incomingResolver return nil }, + Sweeper: newMockSweeper(), }, PutResolverReport: func(_ kvdb.RwTx, _ *channeldb.ResolverReport) error { @@ -374,10 +377,16 @@ func newIncomingResolverTestContext(t *testing.T, isExit bool) *incomingResolver }, } + res := lnwallet.IncomingHtlcResolution{ + SweepSignDesc: input.SignDescriptor{ + Output: &wire.TxOut{}, + }, + } + c.resolver = &htlcIncomingContestResolver{ htlcSuccessResolver: &htlcSuccessResolver{ contractResolverKit: *newContractResolverKit(cfg), - htlcResolution: lnwallet.IncomingHtlcResolution{}, + htlcResolution: res, htlc: channeldb.HTLC{ Amt: lnwire.MilliSatoshi(testHtlcAmount), RHash: testResHash, @@ -386,6 +395,7 @@ func newIncomingResolverTestContext(t *testing.T, isExit bool) *incomingResolver }, htlcExpiry: testHtlcExpiry, } + c.resolver.initLogger("htlcIncomingContestResolver") return c } @@ -395,6 +405,10 @@ func (i *incomingResolverTestContext) resolve() { i.resolveErr = make(chan error, 1) go func() { var err error + + err = i.resolver.Launch() + require.NoError(i.t, err) + i.nextResolver, err = i.resolver.Resolve() i.resolveErr <- err }() From 47722292c554563d98adf122d1249ec8191659a4 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 20 Jun 2024 22:11:47 +0800 Subject: [PATCH 46/92] contractcourt: add `Launch` method to outgoing contest resolver --- .../htlc_outgoing_contest_resolver.go | 44 ++++++++++++++++--- .../htlc_outgoing_contest_resolver_test.go | 6 +++ 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/contractcourt/htlc_outgoing_contest_resolver.go b/contractcourt/htlc_outgoing_contest_resolver.go index 7adce5a6893..b23259b5bb6 100644 --- a/contractcourt/htlc_outgoing_contest_resolver.go +++ b/contractcourt/htlc_outgoing_contest_resolver.go @@ -1,7 +1,6 @@ package contractcourt import ( - "fmt" "io" "github.com/btcsuite/btcd/btcutil" @@ -36,6 +35,37 @@ func newOutgoingContestResolver(res lnwallet.OutgoingHtlcResolution, } } +// Launch will call the inner resolver's launch method if the expiry height has +// been reached, otherwise it's a no-op. +func (h *htlcOutgoingContestResolver) Launch() error { + // NOTE: we don't mark this resolver as launched as the inner resolver + // will set it when it's launched. + if h.launched { + h.log.Tracef("already launched") + return nil + } + + h.log.Debugf("launching contest resolver...") + + _, bestHeight, err := h.ChainIO.GetBestBlock() + if err != nil { + return err + } + + if uint32(bestHeight) < h.htlcResolution.Expiry { + return nil + } + + // If the current height is >= expiry, then a timeout path spend will + // be valid to be included in the next block, and we can immediately + // return the resolver. + h.log.Infof("expired (height=%v, expiry=%v), transforming into "+ + "timeout resolver and launching it", bestHeight, + h.htlcResolution.Expiry) + + return h.htlcTimeoutResolver.Launch() +} + // Resolve commences the resolution of this contract. As this contract hasn't // yet timed out, we'll wait for one of two things to happen // @@ -53,6 +83,7 @@ func (h *htlcOutgoingContestResolver) Resolve() (ContractResolver, error) { // If we're already full resolved, then we don't have anything further // to do. if h.resolved { + h.log.Errorf("already resolved") return nil, nil } @@ -86,7 +117,6 @@ func (h *htlcOutgoingContestResolver) Resolve() (ContractResolver, error) { return nil, errResolverShuttingDown } - // TODO(roasbeef): Checkpoint? return nil, h.claimCleanUp(commitSpend) // If it hasn't, then we'll watch for both the expiration, and the @@ -124,12 +154,14 @@ func (h *htlcOutgoingContestResolver) Resolve() (ContractResolver, error) { // finalized` will be returned and the broadcast will // fail. newHeight := uint32(newBlock.Height) - if newHeight >= h.htlcResolution.Expiry { - log.Infof("%T(%v): HTLC has expired "+ + expiry := h.htlcResolution.Expiry + if newHeight >= expiry { + h.log.Infof("HTLC about to expire "+ "(height=%v, expiry=%v), transforming "+ "into timeout resolver", h, h.htlcResolution.ClaimOutpoint, newHeight, h.htlcResolution.Expiry) + return h.htlcTimeoutResolver, nil } @@ -147,7 +179,7 @@ func (h *htlcOutgoingContestResolver) Resolve() (ContractResolver, error) { return nil, h.claimCleanUp(commitSpend) case <-h.quit: - return nil, fmt.Errorf("resolver canceled") + return nil, errResolverShuttingDown } } } @@ -178,6 +210,8 @@ func (h *htlcOutgoingContestResolver) report() *ContractReport { // // NOTE: Part of the ContractResolver interface. func (h *htlcOutgoingContestResolver) Stop() { + h.log.Debugf("stopping...") + defer h.log.Debugf("stopped") close(h.quit) } diff --git a/contractcourt/htlc_outgoing_contest_resolver_test.go b/contractcourt/htlc_outgoing_contest_resolver_test.go index 4fa3a6874f9..625df60bf1f 100644 --- a/contractcourt/htlc_outgoing_contest_resolver_test.go +++ b/contractcourt/htlc_outgoing_contest_resolver_test.go @@ -15,6 +15,7 @@ import ( "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwire" + "github.com/stretchr/testify/require" ) const ( @@ -159,6 +160,7 @@ func newOutgoingResolverTestContext(t *testing.T) *outgoingResolverTestContext { return nil }, + ChainIO: &mock.ChainIO{}, }, PutResolverReport: func(_ kvdb.RwTx, _ *channeldb.ResolverReport) error { @@ -195,6 +197,7 @@ func newOutgoingResolverTestContext(t *testing.T) *outgoingResolverTestContext { }, }, } + resolver.initLogger("htlcOutgoingContestResolver") return &outgoingResolverTestContext{ resolver: resolver, @@ -209,6 +212,9 @@ func (i *outgoingResolverTestContext) resolve() { // Start resolver. i.resolverResultChan = make(chan resolveResult, 1) go func() { + err := i.resolver.Launch() + require.NoError(i.t, err) + nextResolver, err := i.resolver.Resolve() i.resolverResultChan <- resolveResult{ nextResolver: nextResolver, From 4f5ccb865029865797fae65182f619ebe87e03ed Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Wed, 10 Jul 2024 18:08:23 +0800 Subject: [PATCH 47/92] contractcourt: fix concurrent access to `resolved` This commit makes `resolved` an atomic bool to avoid data race. This field is now defined in `contractResolverKit` to avoid code duplication. --- contractcourt/anchor_resolver.go | 17 +---- contractcourt/breach_resolver.go | 22 +++--- contractcourt/briefcase_test.go | 67 +++++++++++-------- contractcourt/commit_sweep_resolver.go | 26 +++---- contractcourt/contract_resolver.go | 17 +++++ .../htlc_incoming_contest_resolver.go | 19 ++---- .../htlc_outgoing_contest_resolver.go | 10 +-- contractcourt/htlc_success_resolver.go | 27 +++----- contractcourt/htlc_success_resolver_test.go | 4 +- contractcourt/htlc_timeout_resolver.go | 29 ++++---- contractcourt/htlc_timeout_resolver_test.go | 2 +- 11 files changed, 111 insertions(+), 129 deletions(-) diff --git a/contractcourt/anchor_resolver.go b/contractcourt/anchor_resolver.go index ded3ebd83f2..b70e28323d4 100644 --- a/contractcourt/anchor_resolver.go +++ b/contractcourt/anchor_resolver.go @@ -24,9 +24,6 @@ type anchorResolver struct { // anchor is the outpoint on the commitment transaction. anchor wire.OutPoint - // resolved reflects if the contract has been fully resolved or not. - resolved bool - // broadcastHeight is the height that the original contract was // broadcast to the main-chain at. We'll use this value to bound any // historical queries to the chain for spends/confirmations. @@ -89,7 +86,7 @@ func (c *anchorResolver) ResolverKey() []byte { // NOTE: Part of the ContractResolver interface. func (c *anchorResolver) Resolve() (ContractResolver, error) { // If we're already resolved, then we can exit early. - if c.resolved { + if c.IsResolved() { c.log.Errorf("already resolved") return nil, nil } @@ -139,7 +136,7 @@ func (c *anchorResolver) Resolve() (ContractResolver, error) { ) c.reportLock.Unlock() - c.resolved = true + c.markResolved() return nil, c.PutResolverReport(nil, report) } @@ -154,14 +151,6 @@ func (c *anchorResolver) Stop() { close(c.quit) } -// IsResolved returns true if the stored state in the resolve is fully -// resolved. In this case the target output can be forgotten. -// -// NOTE: Part of the ContractResolver interface. -func (c *anchorResolver) IsResolved() bool { - return c.resolved -} - // SupplementState allows the user of a ContractResolver to supplement it with // state required for the proper resolution of a contract. // @@ -198,7 +187,7 @@ func (c *anchorResolver) Launch() error { c.launched = true // If we're already resolved, then we can exit early. - if c.resolved { + if c.IsResolved() { c.log.Errorf("already resolved") return nil } diff --git a/contractcourt/breach_resolver.go b/contractcourt/breach_resolver.go index 1f74924d397..66a974e67f4 100644 --- a/contractcourt/breach_resolver.go +++ b/contractcourt/breach_resolver.go @@ -12,9 +12,6 @@ import ( // future, this will likely take over the duties the current BreachArbitrator // has. type breachResolver struct { - // resolved reflects if the contract has been fully resolved or not. - resolved bool - // subscribed denotes whether or not the breach resolver has subscribed // to the BreachArbitrator for breach resolution. subscribed bool @@ -62,7 +59,7 @@ func (b *breachResolver) Resolve() (ContractResolver, error) { // If the breach resolution process is already complete, then // we can cleanup and checkpoint the resolved state. if complete { - b.resolved = true + b.markResolved() return nil, b.Checkpoint(b) } @@ -75,8 +72,9 @@ func (b *breachResolver) Resolve() (ContractResolver, error) { // The replyChan has been closed, signalling that the breach // has been fully resolved. Checkpoint the resolved state and // exit. - b.resolved = true + b.markResolved() return nil, b.Checkpoint(b) + case <-b.quit: } @@ -89,19 +87,13 @@ func (b *breachResolver) Stop() { close(b.quit) } -// IsResolved returns true if the breachResolver is fully resolved and cleanup -// can occur. -func (b *breachResolver) IsResolved() bool { - return b.resolved -} - // SupplementState adds additional state to the breachResolver. func (b *breachResolver) SupplementState(_ *channeldb.OpenChannel) { } // Encode encodes the breachResolver to the passed writer. func (b *breachResolver) Encode(w io.Writer) error { - return binary.Write(w, endian, b.resolved) + return binary.Write(w, endian, b.IsResolved()) } // newBreachResolverFromReader attempts to decode an encoded breachResolver @@ -114,9 +106,13 @@ func newBreachResolverFromReader(r io.Reader, resCfg ResolverConfig) ( replyChan: make(chan struct{}), } - if err := binary.Read(r, endian, &b.resolved); err != nil { + var resolved bool + if err := binary.Read(r, endian, &resolved); err != nil { return nil, err } + if resolved { + b.markResolved() + } b.initLogger(fmt.Sprintf("%T(%v)", b, b.ChanPoint)) diff --git a/contractcourt/briefcase_test.go b/contractcourt/briefcase_test.go index 533d0eff782..aa2e711efc2 100644 --- a/contractcourt/briefcase_test.go +++ b/contractcourt/briefcase_test.go @@ -206,8 +206,8 @@ func assertResolversEqual(t *testing.T, originalResolver ContractResolver, ogRes.outputIncubating, diskRes.outputIncubating) } if ogRes.resolved != diskRes.resolved { - t.Fatalf("expected %v, got %v", ogRes.resolved, - diskRes.resolved) + t.Fatalf("expected %v, got %v", ogRes.resolved.Load(), + diskRes.resolved.Load()) } if ogRes.broadcastHeight != diskRes.broadcastHeight { t.Fatalf("expected %v, got %v", @@ -229,8 +229,8 @@ func assertResolversEqual(t *testing.T, originalResolver ContractResolver, ogRes.outputIncubating, diskRes.outputIncubating) } if ogRes.resolved != diskRes.resolved { - t.Fatalf("expected %v, got %v", ogRes.resolved, - diskRes.resolved) + t.Fatalf("expected %v, got %v", ogRes.resolved.Load(), + diskRes.resolved.Load()) } if ogRes.broadcastHeight != diskRes.broadcastHeight { t.Fatalf("expected %v, got %v", @@ -275,8 +275,8 @@ func assertResolversEqual(t *testing.T, originalResolver ContractResolver, ogRes.commitResolution, diskRes.commitResolution) } if ogRes.resolved != diskRes.resolved { - t.Fatalf("expected %v, got %v", ogRes.resolved, - diskRes.resolved) + t.Fatalf("expected %v, got %v", ogRes.resolved.Load(), + diskRes.resolved.Load()) } if ogRes.broadcastHeight != diskRes.broadcastHeight { t.Fatalf("expected %v, got %v", @@ -312,13 +312,14 @@ func TestContractInsertionRetrieval(t *testing.T) { SweepSignDesc: testSignDesc, }, outputIncubating: true, - resolved: true, broadcastHeight: 102, htlc: channeldb.HTLC{ HtlcIndex: 12, }, } - successResolver := htlcSuccessResolver{ + timeoutResolver.resolved.Store(true) + + successResolver := &htlcSuccessResolver{ htlcResolution: lnwallet.IncomingHtlcResolution{ Preimage: testPreimage, SignedSuccessTx: nil, @@ -327,40 +328,49 @@ func TestContractInsertionRetrieval(t *testing.T) { SweepSignDesc: testSignDesc, }, outputIncubating: true, - resolved: true, broadcastHeight: 109, htlc: channeldb.HTLC{ RHash: testPreimage, }, } - resolvers := []ContractResolver{ - &timeoutResolver, - &successResolver, - &commitSweepResolver{ - commitResolution: lnwallet.CommitOutputResolution{ - SelfOutPoint: testChanPoint2, - SelfOutputSignDesc: testSignDesc, - MaturityDelay: 99, - }, - resolved: false, - broadcastHeight: 109, - chanPoint: testChanPoint1, + successResolver.resolved.Store(true) + + commitResolver := &commitSweepResolver{ + commitResolution: lnwallet.CommitOutputResolution{ + SelfOutPoint: testChanPoint2, + SelfOutputSignDesc: testSignDesc, + MaturityDelay: 99, }, + broadcastHeight: 109, + chanPoint: testChanPoint1, + } + commitResolver.resolved.Store(false) + + resolvers := []ContractResolver{ + &timeoutResolver, successResolver, commitResolver, } // All resolvers require a unique ResolverKey() output. To achieve this // for the composite resolvers, we'll mutate the underlying resolver // with a new outpoint. - contestTimeout := timeoutResolver - contestTimeout.htlcResolution.ClaimOutpoint = randOutPoint() + contestTimeout := htlcTimeoutResolver{ + htlcResolution: lnwallet.OutgoingHtlcResolution{ + ClaimOutpoint: randOutPoint(), + SweepSignDesc: testSignDesc, + }, + } resolvers = append(resolvers, &htlcOutgoingContestResolver{ htlcTimeoutResolver: &contestTimeout, }) - contestSuccess := successResolver - contestSuccess.htlcResolution.ClaimOutpoint = randOutPoint() + contestSuccess := &htlcSuccessResolver{ + htlcResolution: lnwallet.IncomingHtlcResolution{ + ClaimOutpoint: randOutPoint(), + SweepSignDesc: testSignDesc, + }, + } resolvers = append(resolvers, &htlcIncomingContestResolver{ htlcExpiry: 100, - htlcSuccessResolver: &contestSuccess, + htlcSuccessResolver: contestSuccess, }) // For quick lookup during the test, we'll create this map which allow @@ -438,12 +448,12 @@ func TestContractResolution(t *testing.T) { SweepSignDesc: testSignDesc, }, outputIncubating: true, - resolved: true, broadcastHeight: 192, htlc: channeldb.HTLC{ HtlcIndex: 9912, }, } + timeoutResolver.resolved.Store(true) // First, we'll insert the resolver into the database and ensure that // we get the same resolver out the other side. We do not need to apply @@ -491,12 +501,13 @@ func TestContractSwapping(t *testing.T) { SweepSignDesc: testSignDesc, }, outputIncubating: true, - resolved: true, broadcastHeight: 102, htlc: channeldb.HTLC{ HtlcIndex: 12, }, } + timeoutResolver.resolved.Store(true) + contestResolver := &htlcOutgoingContestResolver{ htlcTimeoutResolver: timeoutResolver, } diff --git a/contractcourt/commit_sweep_resolver.go b/contractcourt/commit_sweep_resolver.go index b3323158db0..84efdeb067b 100644 --- a/contractcourt/commit_sweep_resolver.go +++ b/contractcourt/commit_sweep_resolver.go @@ -39,9 +39,6 @@ type commitSweepResolver struct { // this HTLC on-chain. commitResolution lnwallet.CommitOutputResolution - // resolved reflects if the contract has been fully resolved or not. - resolved bool - // broadcastHeight is the height that the original contract was // broadcast to the main-chain at. We'll use this value to bound any // historical queries to the chain for spends/confirmations. @@ -171,7 +168,7 @@ func (c *commitSweepResolver) getCommitTxConfHeight() (uint32, error) { //nolint:funlen func (c *commitSweepResolver) Resolve() (ContractResolver, error) { // If we're already resolved, then we can exit early. - if c.resolved { + if c.IsResolved() { c.log.Errorf("already resolved") return nil, nil } @@ -224,7 +221,7 @@ func (c *commitSweepResolver) Resolve() (ContractResolver, error) { report := c.currentReport.resolverReport( &sweepTxID, channeldb.ResolverTypeCommit, outcome, ) - c.resolved = true + c.markResolved() // Checkpoint the resolver with a closure that will write the outcome // of the resolver and its sweep transaction to disk. @@ -241,14 +238,6 @@ func (c *commitSweepResolver) Stop() { close(c.quit) } -// IsResolved returns true if the stored state in the resolve is fully -// resolved. In this case the target output can be forgotten. -// -// NOTE: Part of the ContractResolver interface. -func (c *commitSweepResolver) IsResolved() bool { - return c.resolved -} - // SupplementState allows the user of a ContractResolver to supplement it with // state required for the proper resolution of a contract. // @@ -277,7 +266,7 @@ func (c *commitSweepResolver) Encode(w io.Writer) error { return err } - if err := binary.Write(w, endian, c.resolved); err != nil { + if err := binary.Write(w, endian, c.IsResolved()); err != nil { return err } if err := binary.Write(w, endian, c.broadcastHeight); err != nil { @@ -312,9 +301,14 @@ func newCommitSweepResolverFromReader(r io.Reader, resCfg ResolverConfig) ( return nil, err } - if err := binary.Read(r, endian, &c.resolved); err != nil { + var resolved bool + if err := binary.Read(r, endian, &resolved); err != nil { return nil, err } + if resolved { + c.markResolved() + } + if err := binary.Read(r, endian, &c.broadcastHeight); err != nil { return nil, err } @@ -383,7 +377,7 @@ func (c *commitSweepResolver) Launch() error { c.launched = true // If we're already resolved, then we can exit early. - if c.resolved { + if c.IsResolved() { c.log.Errorf("already resolved") return nil } diff --git a/contractcourt/contract_resolver.go b/contractcourt/contract_resolver.go index 814c02ff564..5311d466aee 100644 --- a/contractcourt/contract_resolver.go +++ b/contractcourt/contract_resolver.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io" + "sync/atomic" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btclog/v2" @@ -119,6 +120,9 @@ type contractResolverKit struct { // launched specifies whether the resolver has been launched. Calling // `Launch` will be a no-op if this is true. launched bool + + // resolved reflects if the contract has been fully resolved or not. + resolved atomic.Bool } // newContractResolverKit instantiates the mix-in struct. @@ -137,6 +141,19 @@ func (r *contractResolverKit) initLogger(prefix string) { r.log = log.WithPrefix(logPrefix) } +// IsResolved returns true if the stored state in the resolve is fully +// resolved. In this case the target output can be forgotten. +// +// NOTE: Part of the ContractResolver interface. +func (r *contractResolverKit) IsResolved() bool { + return r.resolved.Load() +} + +// markResolved marks the resolver as resolved. +func (r *contractResolverKit) markResolved() { + r.resolved.Store(true) +} + var ( // errResolverShuttingDown is returned when the resolver stops // progressing because it received the quit signal. diff --git a/contractcourt/htlc_incoming_contest_resolver.go b/contractcourt/htlc_incoming_contest_resolver.go index 20904545746..7dfcc6689b9 100644 --- a/contractcourt/htlc_incoming_contest_resolver.go +++ b/contractcourt/htlc_incoming_contest_resolver.go @@ -124,7 +124,7 @@ func (h *htlcIncomingContestResolver) Launch() error { func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) { // If we're already full resolved, then we don't have anything further // to do. - if h.resolved { + if h.IsResolved() { h.log.Errorf("already resolved") return nil, nil } @@ -140,7 +140,7 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) { // will time it out and get their funds back. This situation // can present itself when we crash before processRemoteAdds in // the link has ran. - h.resolved = true + h.markResolved() if err := h.processFinalHtlcFail(); err != nil { return nil, err @@ -193,7 +193,7 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) { log.Infof("%T(%v): HTLC has timed out (expiry=%v, height=%v), "+ "abandoning", h, h.htlcResolution.ClaimOutpoint, h.htlcExpiry, currentHeight) - h.resolved = true + h.markResolved() if err := h.processFinalHtlcFail(); err != nil { return nil, err @@ -234,7 +234,7 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) { h.htlcResolution.ClaimOutpoint, h.htlcExpiry, currentHeight) - h.resolved = true + h.markResolved() if err := h.processFinalHtlcFail(); err != nil { return nil, err @@ -396,7 +396,8 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) { "(expiry=%v, height=%v), abandoning", h, h.htlcResolution.ClaimOutpoint, h.htlcExpiry, currentHeight) - h.resolved = true + + h.markResolved() if err := h.processFinalHtlcFail(); err != nil { return nil, err @@ -517,14 +518,6 @@ func (h *htlcIncomingContestResolver) Stop() { close(h.quit) } -// IsResolved returns true if the stored state in the resolve is fully -// resolved. In this case the target output can be forgotten. -// -// NOTE: Part of the ContractResolver interface. -func (h *htlcIncomingContestResolver) IsResolved() bool { - return h.resolved -} - // Encode writes an encoded version of the ContractResolver into the passed // Writer. // diff --git a/contractcourt/htlc_outgoing_contest_resolver.go b/contractcourt/htlc_outgoing_contest_resolver.go index b23259b5bb6..8763dfb27eb 100644 --- a/contractcourt/htlc_outgoing_contest_resolver.go +++ b/contractcourt/htlc_outgoing_contest_resolver.go @@ -82,7 +82,7 @@ func (h *htlcOutgoingContestResolver) Launch() error { func (h *htlcOutgoingContestResolver) Resolve() (ContractResolver, error) { // If we're already full resolved, then we don't have anything further // to do. - if h.resolved { + if h.IsResolved() { h.log.Errorf("already resolved") return nil, nil } @@ -215,14 +215,6 @@ func (h *htlcOutgoingContestResolver) Stop() { close(h.quit) } -// IsResolved returns true if the stored state in the resolve is fully -// resolved. In this case the target output can be forgotten. -// -// NOTE: Part of the ContractResolver interface. -func (h *htlcOutgoingContestResolver) IsResolved() bool { - return h.resolved -} - // Encode writes an encoded version of the ContractResolver into the passed // Writer. // diff --git a/contractcourt/htlc_success_resolver.go b/contractcourt/htlc_success_resolver.go index 20b45166343..b4c6db2b916 100644 --- a/contractcourt/htlc_success_resolver.go +++ b/contractcourt/htlc_success_resolver.go @@ -42,9 +42,6 @@ type htlcSuccessResolver struct { // second-level output (true). outputIncubating bool - // resolved reflects if the contract has been fully resolved or not. - resolved bool - // broadcastHeight is the height that the original contract was // broadcast to the main-chain at. We'll use this value to bound any // historical queries to the chain for spends/confirmations. @@ -122,7 +119,7 @@ func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) { switch { // If we're already resolved, then we can exit early. - case h.resolved: + case h.IsResolved(): h.log.Errorf("already resolved") // If this is an output on the remote party's commitment transaction, @@ -226,7 +223,7 @@ func (h *htlcSuccessResolver) checkpointClaim(spendTx *chainhash.Hash) error { } // Finally, we checkpoint the resolver with our report(s). - h.resolved = true + h.markResolved() return h.Checkpoint(h, reports...) } @@ -241,14 +238,6 @@ func (h *htlcSuccessResolver) Stop() { close(h.quit) } -// IsResolved returns true if the stored state in the resolve is fully -// resolved. In this case the target output can be forgotten. -// -// NOTE: Part of the ContractResolver interface. -func (h *htlcSuccessResolver) IsResolved() bool { - return h.resolved -} - // report returns a report on the resolution state of the contract. func (h *htlcSuccessResolver) report() *ContractReport { // If the sign details are nil, the report will be created by handled @@ -298,7 +287,7 @@ func (h *htlcSuccessResolver) Encode(w io.Writer) error { if err := binary.Write(w, endian, h.outputIncubating); err != nil { return err } - if err := binary.Write(w, endian, h.resolved); err != nil { + if err := binary.Write(w, endian, h.IsResolved()); err != nil { return err } if err := binary.Write(w, endian, h.broadcastHeight); err != nil { @@ -337,9 +326,15 @@ func newSuccessResolverFromReader(r io.Reader, resCfg ResolverConfig) ( if err := binary.Read(r, endian, &h.outputIncubating); err != nil { return nil, err } - if err := binary.Read(r, endian, &h.resolved); err != nil { + + var resolved bool + if err := binary.Read(r, endian, &resolved); err != nil { return nil, err } + if resolved { + h.markResolved() + } + if err := binary.Read(r, endian, &h.broadcastHeight); err != nil { return nil, err } @@ -745,7 +740,7 @@ func (h *htlcSuccessResolver) Launch() error { switch { // If we're already resolved, then we can exit early. - case h.resolved: + case h.IsResolved(): h.log.Errorf("already resolved") return nil diff --git a/contractcourt/htlc_success_resolver_test.go b/contractcourt/htlc_success_resolver_test.go index f395d67215a..417ef907ed0 100644 --- a/contractcourt/htlc_success_resolver_test.go +++ b/contractcourt/htlc_success_resolver_test.go @@ -616,11 +616,11 @@ func runFromCheckpoint(t *testing.T, ctx *htlcResolverTestContext, var resolved, incubating bool if h, ok := resolver.(*htlcSuccessResolver); ok { - resolved = h.resolved + resolved = h.resolved.Load() incubating = h.outputIncubating } if h, ok := resolver.(*htlcTimeoutResolver); ok { - resolved = h.resolved + resolved = h.resolved.Load() incubating = h.outputIncubating } diff --git a/contractcourt/htlc_timeout_resolver.go b/contractcourt/htlc_timeout_resolver.go index eae52255f2b..125d205477a 100644 --- a/contractcourt/htlc_timeout_resolver.go +++ b/contractcourt/htlc_timeout_resolver.go @@ -38,9 +38,6 @@ type htlcTimeoutResolver struct { // incubator (utxo nursery). outputIncubating bool - // resolved reflects if the contract has been fully resolved or not. - resolved bool - // broadcastHeight is the height that the original contract was // broadcast to the main-chain at. We'll use this value to bound any // historical queries to the chain for spends/confirmations. @@ -238,7 +235,7 @@ func (h *htlcTimeoutResolver) claimCleanUp( }); err != nil { return err } - h.resolved = true + h.markResolved() // Checkpoint our resolver with a report which reflects the preimage // claim by the remote party. @@ -424,7 +421,7 @@ func checkSizeAndIndex(witness wire.TxWitness, size, index int) bool { // NOTE: Part of the ContractResolver interface. func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) { // If we're already resolved, then we can exit early. - if h.resolved { + if h.IsResolved() { h.log.Errorf("already resolved") return nil, nil } @@ -622,14 +619,6 @@ func (h *htlcTimeoutResolver) Stop() { close(h.quit) } -// IsResolved returns true if the stored state in the resolve is fully -// resolved. In this case the target output can be forgotten. -// -// NOTE: Part of the ContractResolver interface. -func (h *htlcTimeoutResolver) IsResolved() bool { - return h.resolved -} - // report returns a report on the resolution state of the contract. func (h *htlcTimeoutResolver) report() *ContractReport { // If we have a SignedTimeoutTx but no SignDetails, this is a local @@ -689,7 +678,7 @@ func (h *htlcTimeoutResolver) Encode(w io.Writer) error { if err := binary.Write(w, endian, h.outputIncubating); err != nil { return err } - if err := binary.Write(w, endian, h.resolved); err != nil { + if err := binary.Write(w, endian, h.IsResolved()); err != nil { return err } if err := binary.Write(w, endian, h.broadcastHeight); err != nil { @@ -730,9 +719,15 @@ func newTimeoutResolverFromReader(r io.Reader, resCfg ResolverConfig) ( if err := binary.Read(r, endian, &h.outputIncubating); err != nil { return nil, err } - if err := binary.Read(r, endian, &h.resolved); err != nil { + + var resolved bool + if err := binary.Read(r, endian, &resolved); err != nil { return nil, err } + if resolved { + h.markResolved() + } + if err := binary.Read(r, endian, &h.broadcastHeight); err != nil { return nil, err } @@ -1149,7 +1144,7 @@ func (h *htlcTimeoutResolver) checkpointClaim( } // Finally, we checkpoint the resolver with our report(s). - h.resolved = true + h.markResolved() return h.Checkpoint(h, report) } @@ -1285,7 +1280,7 @@ func (h *htlcTimeoutResolver) Launch() error { switch { // If we're already resolved, then we can exit early. - case h.resolved: + case h.IsResolved(): h.log.Errorf("already resolved") return nil diff --git a/contractcourt/htlc_timeout_resolver_test.go b/contractcourt/htlc_timeout_resolver_test.go index 89f334aa83b..017d3d38861 100644 --- a/contractcourt/htlc_timeout_resolver_test.go +++ b/contractcourt/htlc_timeout_resolver_test.go @@ -532,7 +532,7 @@ func testHtlcTimeoutResolver(t *testing.T, testCase htlcTimeoutTestCase) { wg.Wait() // Finally, the resolver should be marked as resolved. - if !resolver.resolved { + if !resolver.resolved.Load() { t.Fatalf("resolver should be marked as resolved") } } From d2e81a19fdaf3d99f65fc76fff5be60cef4d12e1 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 11 Jul 2024 16:19:01 +0800 Subject: [PATCH 48/92] contractcourt: fix concurrent access to `launched` --- contractcourt/anchor_resolver.go | 4 ++-- contractcourt/breach_resolver.go | 4 ++-- contractcourt/commit_sweep_resolver.go | 4 ++-- contractcourt/contract_resolver.go | 17 +++++++++++++++-- contractcourt/htlc_incoming_contest_resolver.go | 2 +- contractcourt/htlc_outgoing_contest_resolver.go | 2 +- contractcourt/htlc_success_resolver.go | 4 ++-- contractcourt/htlc_timeout_resolver.go | 4 ++-- 8 files changed, 27 insertions(+), 14 deletions(-) diff --git a/contractcourt/anchor_resolver.go b/contractcourt/anchor_resolver.go index b70e28323d4..f0f6e2f5a91 100644 --- a/contractcourt/anchor_resolver.go +++ b/contractcourt/anchor_resolver.go @@ -178,13 +178,13 @@ var _ ContractResolver = (*anchorResolver)(nil) // Launch offers the anchor output to the sweeper. func (c *anchorResolver) Launch() error { - if c.launched { + if c.isLaunched() { c.log.Tracef("already launched") return nil } c.log.Debugf("launching resolver...") - c.launched = true + c.markLaunched() // If we're already resolved, then we can exit early. if c.IsResolved() { diff --git a/contractcourt/breach_resolver.go b/contractcourt/breach_resolver.go index 66a974e67f4..f341128006c 100644 --- a/contractcourt/breach_resolver.go +++ b/contractcourt/breach_resolver.go @@ -130,13 +130,13 @@ var _ ContractResolver = (*breachResolver)(nil) // // TODO(yy): implement it once the outputs are offered to the sweeper. func (b *breachResolver) Launch() error { - if b.launched { + if b.isLaunched() { b.log.Tracef("already launched") return nil } b.log.Debugf("launching resolver...") - b.launched = true + b.markLaunched() return nil } diff --git a/contractcourt/commit_sweep_resolver.go b/contractcourt/commit_sweep_resolver.go index 84efdeb067b..55ee08e5d83 100644 --- a/contractcourt/commit_sweep_resolver.go +++ b/contractcourt/commit_sweep_resolver.go @@ -368,13 +368,13 @@ var _ reportingContractResolver = (*commitSweepResolver)(nil) // Launch constructs a commit input and offers it to the sweeper. func (c *commitSweepResolver) Launch() error { - if c.launched { + if c.isLaunched() { c.log.Tracef("already launched") return nil } c.log.Debugf("launching resolver...") - c.launched = true + c.markLaunched() // If we're already resolved, then we can exit early. if c.IsResolved() { diff --git a/contractcourt/contract_resolver.go b/contractcourt/contract_resolver.go index 5311d466aee..293c8496623 100644 --- a/contractcourt/contract_resolver.go +++ b/contractcourt/contract_resolver.go @@ -118,8 +118,11 @@ type contractResolverKit struct { sweepResultChan chan sweep.Result // launched specifies whether the resolver has been launched. Calling - // `Launch` will be a no-op if this is true. - launched bool + // `Launch` will be a no-op if this is true. This value is not saved to + // db, as it's fine to relaunch a resolver after a restart. It's only + // used to avoid resending requests to the sweeper when a new blockbeat + // is received. + launched atomic.Bool // resolved reflects if the contract has been fully resolved or not. resolved atomic.Bool @@ -154,6 +157,16 @@ func (r *contractResolverKit) markResolved() { r.resolved.Store(true) } +// isLaunched returns true if the resolver has been launched. +func (r *contractResolverKit) isLaunched() bool { + return r.launched.Load() +} + +// markLaunched marks the resolver as launched. +func (r *contractResolverKit) markLaunched() { + r.launched.Store(true) +} + var ( // errResolverShuttingDown is returned when the resolver stops // progressing because it received the quit signal. diff --git a/contractcourt/htlc_incoming_contest_resolver.go b/contractcourt/htlc_incoming_contest_resolver.go index 7dfcc6689b9..c1383a947ca 100644 --- a/contractcourt/htlc_incoming_contest_resolver.go +++ b/contractcourt/htlc_incoming_contest_resolver.go @@ -83,7 +83,7 @@ func (h *htlcIncomingContestResolver) processFinalHtlcFail() error { func (h *htlcIncomingContestResolver) Launch() error { // NOTE: we don't mark this resolver as launched as the inner resolver // will set it when it's launched. - if h.launched { + if h.isLaunched() { h.log.Tracef("already launched") return nil } diff --git a/contractcourt/htlc_outgoing_contest_resolver.go b/contractcourt/htlc_outgoing_contest_resolver.go index 8763dfb27eb..19574dee27e 100644 --- a/contractcourt/htlc_outgoing_contest_resolver.go +++ b/contractcourt/htlc_outgoing_contest_resolver.go @@ -40,7 +40,7 @@ func newOutgoingContestResolver(res lnwallet.OutgoingHtlcResolution, func (h *htlcOutgoingContestResolver) Launch() error { // NOTE: we don't mark this resolver as launched as the inner resolver // will set it when it's launched. - if h.launched { + if h.isLaunched() { h.log.Tracef("already launched") return nil } diff --git a/contractcourt/htlc_success_resolver.go b/contractcourt/htlc_success_resolver.go index b4c6db2b916..a4d27ba4e81 100644 --- a/contractcourt/htlc_success_resolver.go +++ b/contractcourt/htlc_success_resolver.go @@ -730,13 +730,13 @@ func (h *htlcSuccessResolver) resolveSuccessTxOutput(op wire.OutPoint) error { // Launch creates an input based on the details of the incoming htlc resolution // and offers it to the sweeper. func (h *htlcSuccessResolver) Launch() error { - if h.launched { + if h.isLaunched() { h.log.Tracef("already launched") return nil } h.log.Debugf("launching resolver...") - h.launched = true + h.markLaunched() switch { // If we're already resolved, then we can exit early. diff --git a/contractcourt/htlc_timeout_resolver.go b/contractcourt/htlc_timeout_resolver.go index 125d205477a..872a3da92b5 100644 --- a/contractcourt/htlc_timeout_resolver.go +++ b/contractcourt/htlc_timeout_resolver.go @@ -1270,13 +1270,13 @@ func (h *htlcTimeoutResolver) resolveTimeoutTxOutput(op wire.OutPoint) error { // Launch creates an input based on the details of the outgoing htlc resolution // and offers it to the sweeper. func (h *htlcTimeoutResolver) Launch() error { - if h.launched { + if h.isLaunched() { h.log.Tracef("already launched") return nil } h.log.Debugf("launching resolver...") - h.launched = true + h.markLaunched() switch { // If we're already resolved, then we can exit early. From 819c15fa0b5208534cd556171bea6b8f765e7ba9 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Wed, 26 Jun 2024 07:41:51 +0800 Subject: [PATCH 49/92] contractcourt: break `launchResolvers` into two steps In this commit, we break the old `launchResolvers` into two steps - step one is to launch the resolvers synchronously, and step two is to actually waiting for the resolvers to be resolved. This is critical as in the following commit we will require the resolvers to be launched at the same blockbeat when a force close event is sent by the chain watcher. --- contractcourt/channel_arbitrator.go | 75 +++++++++++++++++++-- contractcourt/channel_arbitrator_test.go | 54 ++++++++------- contractcourt/contract_resolver.go | 11 +++ contractcourt/htlc_success_resolver_test.go | 3 + 4 files changed, 113 insertions(+), 30 deletions(-) diff --git a/contractcourt/channel_arbitrator.go b/contractcourt/channel_arbitrator.go index b94adf51f3c..3f45c5bfc78 100644 --- a/contractcourt/channel_arbitrator.go +++ b/contractcourt/channel_arbitrator.go @@ -816,7 +816,7 @@ func (c *ChannelArbitrator) relaunchResolvers(commitSet *CommitSet, // TODO(roasbeef): this isn't re-launched? } - c.launchResolvers(unresolvedContracts) + c.resolveContracts(unresolvedContracts) return nil } @@ -1355,7 +1355,7 @@ func (c *ChannelArbitrator) stateStep( // Finally, we'll launch all the required contract resolvers. // Once they're all resolved, we're no longer needed. - c.launchResolvers(resolvers) + c.resolveContracts(resolvers) nextState = StateWaitingFullResolution @@ -1578,18 +1578,75 @@ func (c *ChannelArbitrator) findCommitmentDeadlineAndValue(heightHint uint32, return fn.Some(int32(deadline)), valueLeft, nil } -// launchResolvers updates the activeResolvers list and starts the resolvers. -func (c *ChannelArbitrator) launchResolvers(resolvers []ContractResolver) { +// resolveContracts updates the activeResolvers list and starts to resolve each +// contract concurrently, and launches them. +func (c *ChannelArbitrator) resolveContracts(resolvers []ContractResolver) { c.activeResolversLock.Lock() c.activeResolvers = resolvers c.activeResolversLock.Unlock() + // Launch all resolvers. + c.launchResolvers() + for _, contract := range resolvers { c.wg.Add(1) go c.resolveContract(contract) } } +// launchResolvers launches all the active resolvers concurrently. +func (c *ChannelArbitrator) launchResolvers() { + c.activeResolversLock.Lock() + resolvers := c.activeResolvers + c.activeResolversLock.Unlock() + + // errChans is a map of channels that will be used to receive errors + // returned from launching the resolvers. + errChans := make(map[ContractResolver]chan error, len(resolvers)) + + // Launch each resolver in goroutines. + for _, r := range resolvers { + // If the contract is already resolved, there's no need to + // launch it again. + if r.IsResolved() { + log.Debugf("ChannelArbitrator(%v): skipping resolver "+ + "%T as it's already resolved", c.cfg.ChanPoint, + r) + + continue + } + + // Create a signal chan. + errChan := make(chan error, 1) + errChans[r] = errChan + + go func() { + err := r.Launch() + errChan <- err + }() + } + + // Wait for all resolvers to finish launching. + for r, errChan := range errChans { + select { + case err := <-errChan: + if err == nil { + continue + } + + log.Errorf("ChannelArbitrator(%v): unable to launch "+ + "contract resolver(%T): %v", c.cfg.ChanPoint, r, + err) + + case <-c.quit: + log.Debugf("ChannelArbitrator quit signal received, " + + "exit launchResolvers") + + return + } + } +} + // advanceState is the main driver of our state machine. This method is an // iterative function which repeatedly attempts to advance the internal state // of the channel arbitrator. The state will be advanced until we reach a @@ -2628,6 +2685,13 @@ func (c *ChannelArbitrator) resolveContract(currentContract ContractResolver) { // loop. currentContract = nextContract + // Launch the new contract. + err = currentContract.Launch() + if err != nil { + log.Errorf("Failed to launch %T: %v", + currentContract, err) + } + // If this contract is actually fully resolved, then // we'll mark it as such within the database. case currentContract.IsResolved(): @@ -3144,6 +3208,9 @@ func (c *ChannelArbitrator) handleBlockbeat(beat chainio.Blockbeat) error { } } + // Launch all active resolvers when a new blockbeat is received. + c.launchResolvers() + return nil } diff --git a/contractcourt/channel_arbitrator_test.go b/contractcourt/channel_arbitrator_test.go index d1ef2993d50..827e10b5c7d 100644 --- a/contractcourt/channel_arbitrator_test.go +++ b/contractcourt/channel_arbitrator_test.go @@ -13,6 +13,7 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" + "github.com/davecgh/go-spew/spew" "github.com/lightningnetwork/lnd/chainio" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/channeldb" @@ -1101,12 +1102,7 @@ func TestChannelArbitratorLocalForceClosePendingHtlc(t *testing.T) { } // Send a notification that the expiry height has been reached. - // - // TODO(yy): remove the EpochChan and use the blockbeat below once - // resolvers are hooked with the blockbeat. oldNotifier.EpochChan <- &chainntnfs.BlockEpoch{Height: 10} - // beat := chainio.NewBlockbeatFromHeight(10) - // chanArb.BlockbeatChan <- beat // htlcOutgoingContestResolver is now transforming into a // htlcTimeoutResolver and should send the contract off for incubation. @@ -1149,8 +1145,12 @@ func TestChannelArbitratorLocalForceClosePendingHtlc(t *testing.T) { default: } - // Notify resolver that the second level transaction is spent. - oldNotifier.SpendChan <- &chainntnfs.SpendDetail{SpendingTx: closeTx} + // Notify resolver that the output of the timeout tx has been spent. + oldNotifier.SpendChan <- &chainntnfs.SpendDetail{ + SpendingTx: closeTx, + SpentOutPoint: &wire.OutPoint{}, + SpenderTxHash: &closeTxid, + } // At this point channel should be marked as resolved. chanArbCtxNew.AssertStateTransitions(StateFullyResolved) @@ -2830,27 +2830,28 @@ func TestChannelArbitratorAnchors(t *testing.T) { } chanArb.UpdateContractSignals(signals) - // Set current block height. - beat = newBeatFromHeight(int32(heightHint)) - chanArbCtx.chanArb.BlockbeatChan <- beat - htlcAmt := lnwire.MilliSatoshi(1_000_000) // Create testing HTLCs. - deadlineDelta := uint32(10) - deadlinePreimageDelta := deadlineDelta + 2 + spendingHeight := uint32(beat.Height()) + deadlineDelta := uint32(100) + + deadlinePreimageDelta := deadlineDelta htlcWithPreimage := channeldb.HTLC{ - HtlcIndex: 99, - RefundTimeout: heightHint + deadlinePreimageDelta, + HtlcIndex: 99, + // RefundTimeout is 101. + RefundTimeout: spendingHeight + deadlinePreimageDelta, RHash: rHash, Incoming: true, Amt: htlcAmt, } + expectedDeadline := deadlineDelta/2 + spendingHeight - deadlineHTLCdelta := deadlineDelta + 3 + deadlineHTLCdelta := deadlineDelta + 40 htlc := channeldb.HTLC{ - HtlcIndex: 100, - RefundTimeout: heightHint + deadlineHTLCdelta, + HtlcIndex: 100, + // RefundTimeout is 141. + RefundTimeout: spendingHeight + deadlineHTLCdelta, Amt: htlcAmt, } @@ -2935,7 +2936,9 @@ func TestChannelArbitratorAnchors(t *testing.T) { //nolint:ll chanArb.cfg.ChainEvents.LocalUnilateralClosure <- &LocalUnilateralCloseInfo{ - SpendDetail: &chainntnfs.SpendDetail{}, + SpendDetail: &chainntnfs.SpendDetail{ + SpendingHeight: int32(spendingHeight), + }, LocalForceCloseSummary: &lnwallet.LocalForceCloseSummary{ CloseTx: closeTx, ContractResolutions: fn.Some(lnwallet.ContractResolutions{ @@ -2999,16 +3002,14 @@ func TestChannelArbitratorAnchors(t *testing.T) { // to htlcWithPreimage's CLTV. require.Equal(t, 2, len(chanArbCtx.sweeper.deadlines)) require.EqualValues(t, - heightHint+deadlinePreimageDelta/2, + expectedDeadline, chanArbCtx.sweeper.deadlines[0], "want %d, got %d", - heightHint+deadlinePreimageDelta/2, - chanArbCtx.sweeper.deadlines[0], + expectedDeadline, chanArbCtx.sweeper.deadlines[0], ) require.EqualValues(t, - heightHint+deadlinePreimageDelta/2, + expectedDeadline, chanArbCtx.sweeper.deadlines[1], "want %d, got %d", - heightHint+deadlinePreimageDelta/2, - chanArbCtx.sweeper.deadlines[1], + expectedDeadline, chanArbCtx.sweeper.deadlines[1], ) } @@ -3159,7 +3160,8 @@ func assertResolverReport(t *testing.T, reports chan *channeldb.ResolverReport, select { case report := <-reports: if !reflect.DeepEqual(report, expected) { - t.Fatalf("expected: %v, got: %v", expected, report) + t.Fatalf("expected: %v, got: %v", spew.Sdump(expected), + spew.Sdump(report)) } case <-time.After(defaultTimeout): diff --git a/contractcourt/contract_resolver.go b/contractcourt/contract_resolver.go index 293c8496623..d11bd2f597a 100644 --- a/contractcourt/contract_resolver.go +++ b/contractcourt/contract_resolver.go @@ -37,6 +37,17 @@ type ContractResolver interface { // resides within. ResolverKey() []byte + // Launch starts the resolver by constructing an input and offering it + // to the sweeper. Once offered, it's expected to monitor the sweeping + // result in a goroutine invoked by calling Resolve. + // + // NOTE: We can call `Resolve` inside a goroutine at the end of this + // method to avoid calling it in the ChannelArbitrator. However, there + // are some DB-related operations such as SwapContract/ResolveContract + // which need to be done inside the resolvers instead, which needs a + // deeper refactoring. + Launch() error + // Resolve instructs the contract resolver to resolve the output // on-chain. Once the output has been *fully* resolved, the function // should return immediately with a nil ContractResolver value for the diff --git a/contractcourt/htlc_success_resolver_test.go b/contractcourt/htlc_success_resolver_test.go index 417ef907ed0..fe6ee1ad0ea 100644 --- a/contractcourt/htlc_success_resolver_test.go +++ b/contractcourt/htlc_success_resolver_test.go @@ -146,6 +146,9 @@ func (i *htlcResolverTestContext) resolve() { i.resolverResultChan = make(chan resolveResult, 1) go func() { + err := i.resolver.Launch() + require.NoError(i.t, err) + nextResolver, err := i.resolver.Resolve() i.resolverResultChan <- resolveResult{ nextResolver: nextResolver, From 63aa5aa6e9d685ba93c0b06aca5b18f4e7d1609a Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Mon, 25 Nov 2024 13:55:11 +0800 Subject: [PATCH 50/92] contractcourt: offer outgoing htlc one block earlier before its expiry We need to offer the outgoing htlc one block earlier to make sure when the expiry height hits, the sweeper will not miss sweeping it in the same block. This also means the outgoing contest resolver now only does one thing - watch for preimage spend till height expiry-1, which can easily be moved into the timeout resolver instead in the future. --- contractcourt/htlc_outgoing_contest_resolver.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/contractcourt/htlc_outgoing_contest_resolver.go b/contractcourt/htlc_outgoing_contest_resolver.go index 19574dee27e..b66a3fdf0b3 100644 --- a/contractcourt/htlc_outgoing_contest_resolver.go +++ b/contractcourt/htlc_outgoing_contest_resolver.go @@ -155,7 +155,14 @@ func (h *htlcOutgoingContestResolver) Resolve() (ContractResolver, error) { // fail. newHeight := uint32(newBlock.Height) expiry := h.htlcResolution.Expiry - if newHeight >= expiry { + + // Check if the expiry height is about to be reached. + // We offer this HTLC one block earlier to make sure + // when the next block arrives, the sweeper will pick + // up this input and sweep it immediately. The sweeper + // will handle the waiting for the one last block till + // expiry. + if newHeight >= expiry-1 { h.log.Infof("HTLC about to expire "+ "(height=%v, expiry=%v), transforming "+ "into timeout resolver", h, From 07cb3aef009f164ad179374b204ea610a7791662 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 20 Jun 2024 23:01:36 +0800 Subject: [PATCH 51/92] contractcourt: implement `Consumer` on `chainWatcher` --- contractcourt/chain_watcher.go | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/contractcourt/chain_watcher.go b/contractcourt/chain_watcher.go index e29f21e7f45..348a00b0bb8 100644 --- a/contractcourt/chain_watcher.go +++ b/contractcourt/chain_watcher.go @@ -16,6 +16,7 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/davecgh/go-spew/spew" + "github.com/lightningnetwork/lnd/chainio" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/fn/v2" @@ -210,6 +211,10 @@ type chainWatcher struct { started int32 // To be used atomically. stopped int32 // To be used atomically. + // Embed the blockbeat consumer struct to get access to the method + // `NotifyBlockProcessed` and the `BlockbeatChan`. + chainio.BeatConsumer + quit chan struct{} wg sync.WaitGroup @@ -260,12 +265,27 @@ func newChainWatcher(cfg chainWatcherConfig) (*chainWatcher, error) { ) } - return &chainWatcher{ + c := &chainWatcher{ cfg: cfg, stateHintObfuscator: stateHint, quit: make(chan struct{}), clientSubscriptions: make(map[uint64]*ChainEventSubscription), - }, nil + } + + // Mount the block consumer. + c.BeatConsumer = chainio.NewBeatConsumer(c.quit, c.Name()) + + return c, nil +} + +// Compile-time check for the chainio.Consumer interface. +var _ chainio.Consumer = (*chainWatcher)(nil) + +// Name returns the name of the watcher. +// +// NOTE: part of the `chainio.Consumer` interface. +func (c *chainWatcher) Name() string { + return fmt.Sprintf("ChainWatcher(%v)", c.cfg.chanState.FundingOutpoint) } // Start starts all goroutines that the chainWatcher needs to perform its From c1a9390c36e5dd468e5e3b751f15a9e2bc420f42 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 16 Nov 2024 08:35:28 +0800 Subject: [PATCH 52/92] contractcourt: register spend notification during init This commit moves the creation of the spending notification from `Start` to `newChainWatcher` so we subscribe the spending event before handling the block, which is needed to properly handle the blockbeat. --- contractcourt/chain_watcher.go | 160 +++++++++++++++++++-------------- 1 file changed, 92 insertions(+), 68 deletions(-) diff --git a/contractcourt/chain_watcher.go b/contractcourt/chain_watcher.go index 348a00b0bb8..4ee274210f7 100644 --- a/contractcourt/chain_watcher.go +++ b/contractcourt/chain_watcher.go @@ -241,6 +241,10 @@ type chainWatcher struct { // clientSubscriptions is a map that keeps track of all the active // client subscriptions for events related to this channel. clientSubscriptions map[uint64]*ChainEventSubscription + + // fundingSpendNtfn is the spending notification subscription for the + // funding outpoint. + fundingSpendNtfn *chainntnfs.SpendEvent } // newChainWatcher returns a new instance of a chainWatcher for a channel given @@ -265,11 +269,32 @@ func newChainWatcher(cfg chainWatcherConfig) (*chainWatcher, error) { ) } + // Get the witness script for the funding output. + fundingPkScript, err := deriveFundingPkScript(chanState) + if err != nil { + return nil, err + } + + // Get the channel opening block height. + heightHint := deriveHeightHint(chanState) + + // We'll register for a notification to be dispatched if the funding + // output is spent. + spendNtfn, err := cfg.notifier.RegisterSpendNtfn( + &chanState.FundingOutpoint, fundingPkScript, heightHint, + ) + if err != nil { + return nil, err + } + c := &chainWatcher{ cfg: cfg, stateHintObfuscator: stateHint, quit: make(chan struct{}), clientSubscriptions: make(map[uint64]*ChainEventSubscription), + fundingPkScript: fundingPkScript, + heightHint: heightHint, + fundingSpendNtfn: spendNtfn, } // Mount the block consumer. @@ -295,75 +320,11 @@ func (c *chainWatcher) Start() error { return nil } - chanState := c.cfg.chanState log.Debugf("Starting chain watcher for ChannelPoint(%v)", - chanState.FundingOutpoint) - - // First, we'll register for a notification to be dispatched if the - // funding output is spent. - fundingOut := &chanState.FundingOutpoint - - // As a height hint, we'll try to use the opening height, but if the - // channel isn't yet open, then we'll use the height it was broadcast - // at. This may be an unconfirmed zero-conf channel. - c.heightHint = c.cfg.chanState.ShortChanID().BlockHeight - if c.heightHint == 0 { - c.heightHint = chanState.BroadcastHeight() - } - - // Since no zero-conf state is stored in a channel backup, the below - // logic will not be triggered for restored, zero-conf channels. Set - // the height hint for zero-conf channels. - if chanState.IsZeroConf() { - if chanState.ZeroConfConfirmed() { - // If the zero-conf channel is confirmed, we'll use the - // confirmed SCID's block height. - c.heightHint = chanState.ZeroConfRealScid().BlockHeight - } else { - // The zero-conf channel is unconfirmed. We'll need to - // use the FundingBroadcastHeight. - c.heightHint = chanState.BroadcastHeight() - } - } - - localKey := chanState.LocalChanCfg.MultiSigKey.PubKey - remoteKey := chanState.RemoteChanCfg.MultiSigKey.PubKey - - var ( - err error - ) - if chanState.ChanType.IsTaproot() { - c.fundingPkScript, _, err = input.GenTaprootFundingScript( - localKey, remoteKey, 0, chanState.TapscriptRoot, - ) - if err != nil { - return err - } - } else { - multiSigScript, err := input.GenMultiSigScript( - localKey.SerializeCompressed(), - remoteKey.SerializeCompressed(), - ) - if err != nil { - return err - } - c.fundingPkScript, err = input.WitnessScriptHash(multiSigScript) - if err != nil { - return err - } - } - - spendNtfn, err := c.cfg.notifier.RegisterSpendNtfn( - fundingOut, c.fundingPkScript, c.heightHint, - ) - if err != nil { - return err - } + c.cfg.chanState.FundingOutpoint) - // With the spend notification obtained, we'll now dispatch the - // closeObserver which will properly react to any changes. c.wg.Add(1) - go c.closeObserver(spendNtfn) + go c.closeObserver() return nil } @@ -642,8 +603,9 @@ func newChainSet(chanState *channeldb.OpenChannel) (*chainSet, error) { // close observer will assembled the proper materials required to claim the // funds of the channel on-chain (if required), then dispatch these as // notifications to all subscribers. -func (c *chainWatcher) closeObserver(spendNtfn *chainntnfs.SpendEvent) { +func (c *chainWatcher) closeObserver() { defer c.wg.Done() + defer c.fundingSpendNtfn.Cancel() log.Infof("Close observer for ChannelPoint(%v) active", c.cfg.chanState.FundingOutpoint) @@ -683,7 +645,7 @@ func (c *chainWatcher) closeObserver(spendNtfn *chainntnfs.SpendEvent) { // TODO(Roasbeef): need to be able to ensure this only triggers // on confirmation, to ensure if multiple txns are broadcast, we // act on the one that's timestamped - case commitSpend, ok := <-spendNtfn.Spend: + case commitSpend, ok := <-c.fundingSpendNtfn.Spend: // If the channel was closed, then this means that the notifier // exited, so we will as well. if !ok { @@ -1432,3 +1394,65 @@ func (c *chainWatcher) waitForCommitmentPoint() *btcec.PublicKey { } } } + +// deriveFundingPkScript derives the script used in the funding output. +func deriveFundingPkScript(chanState *channeldb.OpenChannel) ([]byte, error) { + localKey := chanState.LocalChanCfg.MultiSigKey.PubKey + remoteKey := chanState.RemoteChanCfg.MultiSigKey.PubKey + + var ( + err error + fundingPkScript []byte + ) + + if chanState.ChanType.IsTaproot() { + fundingPkScript, _, err = input.GenTaprootFundingScript( + localKey, remoteKey, 0, chanState.TapscriptRoot, + ) + if err != nil { + return nil, err + } + } else { + multiSigScript, err := input.GenMultiSigScript( + localKey.SerializeCompressed(), + remoteKey.SerializeCompressed(), + ) + if err != nil { + return nil, err + } + fundingPkScript, err = input.WitnessScriptHash(multiSigScript) + if err != nil { + return nil, err + } + } + + return fundingPkScript, nil +} + +// deriveHeightHint derives the block height for the channel opening. +func deriveHeightHint(chanState *channeldb.OpenChannel) uint32 { + // As a height hint, we'll try to use the opening height, but if the + // channel isn't yet open, then we'll use the height it was broadcast + // at. This may be an unconfirmed zero-conf channel. + heightHint := chanState.ShortChanID().BlockHeight + if heightHint == 0 { + heightHint = chanState.BroadcastHeight() + } + + // Since no zero-conf state is stored in a channel backup, the below + // logic will not be triggered for restored, zero-conf channels. Set + // the height hint for zero-conf channels. + if chanState.IsZeroConf() { + if chanState.ZeroConfConfirmed() { + // If the zero-conf channel is confirmed, we'll use the + // confirmed SCID's block height. + heightHint = chanState.ZeroConfRealScid().BlockHeight + } else { + // The zero-conf channel is unconfirmed. We'll need to + // use the FundingBroadcastHeight. + heightHint = chanState.BroadcastHeight() + } + } + + return heightHint +} From 4e30598263aeb9c8a1d0be54f5a5768a23cb614f Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 16 Nov 2024 08:42:04 +0800 Subject: [PATCH 53/92] contractcourt: add method `handleCommitSpend` To prepare for the blockbeat handler. --- contractcourt/chain_watcher.go | 210 ++++++++++++++++----------------- 1 file changed, 103 insertions(+), 107 deletions(-) diff --git a/contractcourt/chain_watcher.go b/contractcourt/chain_watcher.go index 4ee274210f7..e54ba8b45c8 100644 --- a/contractcourt/chain_watcher.go +++ b/contractcourt/chain_watcher.go @@ -652,116 +652,11 @@ func (c *chainWatcher) closeObserver() { return } - // Otherwise, the remote party might have broadcast a prior - // revoked state...!!! - commitTxBroadcast := commitSpend.SpendingTx - - // First, we'll construct the chainset which includes all the - // data we need to dispatch an event to our subscribers about - // this possible channel close event. - chainSet, err := newChainSet(c.cfg.chanState) + err := c.handleCommitSpend(commitSpend) if err != nil { - log.Errorf("unable to create commit set: %v", err) - return - } - - // Decode the state hint encoded within the commitment - // transaction to determine if this is a revoked state or not. - obfuscator := c.stateHintObfuscator - broadcastStateNum := c.cfg.extractStateNumHint( - commitTxBroadcast, obfuscator, - ) - - // We'll go on to check whether it could be our own commitment - // that was published and know is confirmed. - ok, err = c.handleKnownLocalState( - commitSpend, broadcastStateNum, chainSet, - ) - if err != nil { - log.Errorf("Unable to handle known local state: %v", - err) - return - } - - if ok { - return + log.Errorf("Failed to handle commit spend: %v", err) } - // Now that we know it is neither a non-cooperative closure nor - // a local close with the latest state, we check if it is the - // remote that closed with any prior or current state. - ok, err = c.handleKnownRemoteState( - commitSpend, broadcastStateNum, chainSet, - ) - if err != nil { - log.Errorf("Unable to handle known remote state: %v", - err) - return - } - - if ok { - return - } - - // Next, we'll check to see if this is a cooperative channel - // closure or not. This is characterized by having an input - // sequence number that's finalized. This won't happen with - // regular commitment transactions due to the state hint - // encoding scheme. - switch commitTxBroadcast.TxIn[0].Sequence { - case wire.MaxTxInSequenceNum: - fallthrough - case mempool.MaxRBFSequence: - // TODO(roasbeef): rare but possible, need itest case - // for - err := c.dispatchCooperativeClose(commitSpend) - if err != nil { - log.Errorf("unable to handle co op close: %v", err) - } - return - } - - log.Warnf("Unknown commitment broadcast for "+ - "ChannelPoint(%v) ", c.cfg.chanState.FundingOutpoint) - - // We'll try to recover as best as possible from losing state. - // We first check if this was a local unknown state. This could - // happen if we force close, then lose state or attempt - // recovery before the commitment confirms. - ok, err = c.handleUnknownLocalState( - commitSpend, broadcastStateNum, chainSet, - ) - if err != nil { - log.Errorf("Unable to handle known local state: %v", - err) - return - } - - if ok { - return - } - - // Since it was neither a known remote state, nor a local state - // that was published, it most likely mean we lost state and - // the remote node closed. In this case we must start the DLP - // protocol in hope of getting our money back. - ok, err = c.handleUnknownRemoteState( - commitSpend, broadcastStateNum, chainSet, - ) - if err != nil { - log.Errorf("Unable to handle unknown remote state: %v", - err) - return - } - - if ok { - return - } - - log.Warnf("Unable to handle spending tx %v of channel point %v", - commitTxBroadcast.TxHash(), c.cfg.chanState.FundingOutpoint) - return - // The chainWatcher has been signalled to exit, so we'll do so now. case <-c.quit: return @@ -1456,3 +1351,104 @@ func deriveHeightHint(chanState *channeldb.OpenChannel) uint32 { return heightHint } + +// handleCommitSpend takes a spending tx of the funding output and handles the +// channel close based on the closure type. +func (c *chainWatcher) handleCommitSpend( + commitSpend *chainntnfs.SpendDetail) error { + + commitTxBroadcast := commitSpend.SpendingTx + + // First, we'll construct the chainset which includes all the data we + // need to dispatch an event to our subscribers about this possible + // channel close event. + chainSet, err := newChainSet(c.cfg.chanState) + if err != nil { + return fmt.Errorf("create commit set: %w", err) + } + + // Decode the state hint encoded within the commitment transaction to + // determine if this is a revoked state or not. + obfuscator := c.stateHintObfuscator + broadcastStateNum := c.cfg.extractStateNumHint( + commitTxBroadcast, obfuscator, + ) + + // We'll go on to check whether it could be our own commitment that was + // published and know is confirmed. + ok, err := c.handleKnownLocalState( + commitSpend, broadcastStateNum, chainSet, + ) + if err != nil { + return fmt.Errorf("handle known local state: %w", err) + } + if ok { + return nil + } + + // Now that we know it is neither a non-cooperative closure nor a local + // close with the latest state, we check if it is the remote that + // closed with any prior or current state. + ok, err = c.handleKnownRemoteState( + commitSpend, broadcastStateNum, chainSet, + ) + if err != nil { + return fmt.Errorf("handle known remote state: %w", err) + } + if ok { + return nil + } + + // Next, we'll check to see if this is a cooperative channel closure or + // not. This is characterized by having an input sequence number that's + // finalized. This won't happen with regular commitment transactions + // due to the state hint encoding scheme. + switch commitTxBroadcast.TxIn[0].Sequence { + case wire.MaxTxInSequenceNum: + fallthrough + case mempool.MaxRBFSequence: + // TODO(roasbeef): rare but possible, need itest case for + err := c.dispatchCooperativeClose(commitSpend) + if err != nil { + return fmt.Errorf("handle coop close: %w", err) + } + + return nil + } + + log.Warnf("Unknown commitment broadcast for ChannelPoint(%v) ", + c.cfg.chanState.FundingOutpoint) + + // We'll try to recover as best as possible from losing state. We + // first check if this was a local unknown state. This could happen if + // we force close, then lose state or attempt recovery before the + // commitment confirms. + ok, err = c.handleUnknownLocalState( + commitSpend, broadcastStateNum, chainSet, + ) + if err != nil { + return fmt.Errorf("handle known local state: %w", err) + } + if ok { + return nil + } + + // Since it was neither a known remote state, nor a local state that + // was published, it most likely mean we lost state and the remote node + // closed. In this case we must start the DLP protocol in hope of + // getting our money back. + ok, err = c.handleUnknownRemoteState( + commitSpend, broadcastStateNum, chainSet, + ) + if err != nil { + return fmt.Errorf("handle unknown remote state: %w", err) + } + if ok { + return nil + } + + log.Errorf("Unable to handle spending tx %v of channel point %v", + commitTxBroadcast.TxHash(), c.cfg.chanState.FundingOutpoint) + + return nil +} From 8237598ed17449e6ea3363357d9783d59cedbba2 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 16 Nov 2024 08:56:28 +0800 Subject: [PATCH 54/92] contractcourt: handle blockbeat in `chainWatcher` --- contractcourt/chain_watcher.go | 169 +++++++++++++++++++++------- contractcourt/chain_watcher_test.go | 125 ++++++++++++++++++-- 2 files changed, 242 insertions(+), 52 deletions(-) diff --git a/contractcourt/chain_watcher.go b/contractcourt/chain_watcher.go index e54ba8b45c8..cd8d6f3542a 100644 --- a/contractcourt/chain_watcher.go +++ b/contractcourt/chain_watcher.go @@ -536,7 +536,7 @@ func newChainSet(chanState *channeldb.OpenChannel) (*chainSet, error) { localCommit, remoteCommit, err := chanState.LatestCommitments() if err != nil { return nil, fmt.Errorf("unable to fetch channel state for "+ - "chan_point=%v", chanState.FundingOutpoint) + "chan_point=%v: %v", chanState.FundingOutpoint, err) } log.Tracef("ChannelPoint(%v): local_commit_type=%v, local_commit=%v", @@ -610,56 +610,42 @@ func (c *chainWatcher) closeObserver() { log.Infof("Close observer for ChannelPoint(%v) active", c.cfg.chanState.FundingOutpoint) - // If this is a taproot channel, before we proceed, we want to ensure - // that the expected funding output has confirmed on chain. - if c.cfg.chanState.ChanType.IsTaproot() { - fundingPoint := c.cfg.chanState.FundingOutpoint - - confNtfn, err := c.cfg.notifier.RegisterConfirmationsNtfn( - &fundingPoint.Hash, c.fundingPkScript, 1, c.heightHint, - ) - if err != nil { - log.Warnf("unable to register for conf: %v", err) - } - - log.Infof("Waiting for taproot ChannelPoint(%v) to confirm...", - c.cfg.chanState.FundingOutpoint) - + for { select { - case _, ok := <-confNtfn.Confirmed: + // A new block is received, we will check whether this block + // contains a spending tx that we are interested in. + case beat := <-c.BlockbeatChan: + log.Debugf("ChainWatcher(%v) received blockbeat %v", + c.cfg.chanState.FundingOutpoint, beat.Height()) + + // Process the block. + c.handleBlockbeat(beat) + + // If the funding outpoint is spent, we now go ahead and handle + // it. Note that we cannot rely solely on the `block` event + // above to trigger a close event, as deep down, the receiving + // of block notifications and the receiving of spending + // notifications are done in two different goroutines, so the + // expected order: [receive block -> receive spend] is not + // guaranteed . + case spend, ok := <-c.fundingSpendNtfn.Spend: // If the channel was closed, then this means that the // notifier exited, so we will as well. if !ok { return } - case <-c.quit: - return - } - } - select { - // We've detected a spend of the channel onchain! Depending on the type - // of spend, we'll act accordingly, so we'll examine the spending - // transaction to determine what we should do. - // - // TODO(Roasbeef): need to be able to ensure this only triggers - // on confirmation, to ensure if multiple txns are broadcast, we - // act on the one that's timestamped - case commitSpend, ok := <-c.fundingSpendNtfn.Spend: - // If the channel was closed, then this means that the notifier - // exited, so we will as well. - if !ok { - return - } + err := c.handleCommitSpend(spend) + if err != nil { + log.Errorf("Failed to handle commit spend: %v", + err) + } - err := c.handleCommitSpend(commitSpend) - if err != nil { - log.Errorf("Failed to handle commit spend: %v", err) + // The chainWatcher has been signalled to exit, so we'll do so + // now. + case <-c.quit: + return } - - // The chainWatcher has been signalled to exit, so we'll do so now. - case <-c.quit: - return } } @@ -1452,3 +1438,102 @@ func (c *chainWatcher) handleCommitSpend( return nil } + +// checkFundingSpend performs a non-blocking read on the spendNtfn channel to +// check whether there's a commit spend already. Returns the spend details if +// found. +func (c *chainWatcher) checkFundingSpend() *chainntnfs.SpendDetail { + select { + // We've detected a spend of the channel onchain! Depending on the type + // of spend, we'll act accordingly, so we'll examine the spending + // transaction to determine what we should do. + // + // TODO(Roasbeef): need to be able to ensure this only triggers + // on confirmation, to ensure if multiple txns are broadcast, we + // act on the one that's timestamped + case spend, ok := <-c.fundingSpendNtfn.Spend: + // If the channel was closed, then this means that the notifier + // exited, so we will as well. + if !ok { + return nil + } + + log.Debugf("Found spend details for funding output: %v", + spend.SpenderTxHash) + + return spend + + default: + } + + return nil +} + +// chanPointConfirmed checks whether the given channel point has confirmed. +// This is used to ensure that the funding output has confirmed on chain before +// we proceed with the rest of the close observer logic for taproot channels. +func (c *chainWatcher) chanPointConfirmed() bool { + op := c.cfg.chanState.FundingOutpoint + confNtfn, err := c.cfg.notifier.RegisterConfirmationsNtfn( + &op.Hash, c.fundingPkScript, 1, c.heightHint, + ) + if err != nil { + log.Errorf("Unable to register for conf: %v", err) + + return false + } + + select { + case _, ok := <-confNtfn.Confirmed: + // If the channel was closed, then this means that the notifier + // exited, so we will as well. + if !ok { +// Check the docs in `fundingConfirmedNtfn` for details. + return false + } + + log.Debugf("Taproot ChannelPoint(%v) confirmed", op) + + return true + + default: + log.Infof("Taproot ChannelPoint(%v) not confirmed yet", op) + + return false + } +} + +// handleBlockbeat takes a blockbeat and queries for a spending tx for the +// funding output. If the spending tx is found, it will be handled based on the +// closure type. +func (c *chainWatcher) handleBlockbeat(beat chainio.Blockbeat) { + // Notify the chain watcher has processed the block. + defer c.NotifyBlockProcessed(beat, nil) + + // If this is a taproot channel, before we proceed, we want to ensure + // that the expected funding output has confirmed on chain. + if c.cfg.chanState.IsPending && c.cfg.chanState.ChanType.IsTaproot() { + // If the funding output hasn't confirmed in this block, we + // will check it again in the next block. + if !c.chanPointConfirmed() { + return + } + } + + // Perform a non-blocking read to check whether the funding output was + // spent. + spend := c.checkFundingSpend() + if spend == nil { + log.Tracef("No spend found for ChannelPoint(%v) in block %v", + c.cfg.chanState.FundingOutpoint, beat.Height()) + + return + } + + // The funding output was spent, we now handle it by sending a close + // event to the channel arbitrator. + err := c.handleCommitSpend(spend) + if err != nil { + log.Errorf("Failed to handle commit spend: %v", err) + } +} diff --git a/contractcourt/chain_watcher_test.go b/contractcourt/chain_watcher_test.go index baea8d8738c..0b3e23e714e 100644 --- a/contractcourt/chain_watcher_test.go +++ b/contractcourt/chain_watcher_test.go @@ -9,10 +9,11 @@ import ( "time" "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/chainio" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/input" - "github.com/lightningnetwork/lnd/lntest/mock" + lnmock "github.com/lightningnetwork/lnd/lntest/mock" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwire" "github.com/stretchr/testify/require" @@ -33,8 +34,8 @@ func TestChainWatcherRemoteUnilateralClose(t *testing.T) { // With the channels created, we'll now create a chain watcher instance // which will be watching for any closes of Alice's channel. - aliceNotifier := &mock.ChainNotifier{ - SpendChan: make(chan *chainntnfs.SpendDetail), + aliceNotifier := &lnmock.ChainNotifier{ + SpendChan: make(chan *chainntnfs.SpendDetail, 1), EpochChan: make(chan *chainntnfs.BlockEpoch), ConfChan: make(chan *chainntnfs.TxConfirmation), } @@ -49,6 +50,20 @@ func TestChainWatcherRemoteUnilateralClose(t *testing.T) { require.NoError(t, err, "unable to start chain watcher") defer aliceChainWatcher.Stop() + // Create a mock blockbeat and send it to Alice's BlockbeatChan. + mockBeat := &chainio.MockBlockbeat{} + + // Mock the logger. We don't care how many times it's called as it's + // not critical. + mockBeat.On("logger").Return(log) + + // Mock a fake block height - this is called based on the debuglevel. + mockBeat.On("Height").Return(int32(1)).Maybe() + + // Mock `NotifyBlockProcessed` to be call once. + mockBeat.On("NotifyBlockProcessed", + nil, aliceChainWatcher.quit).Return().Once() + // We'll request a new channel event subscription from Alice's chain // watcher. chanEvents := aliceChainWatcher.SubscribeChannelEvents() @@ -61,7 +76,19 @@ func TestChainWatcherRemoteUnilateralClose(t *testing.T) { SpenderTxHash: &bobTxHash, SpendingTx: bobCommit, } - aliceNotifier.SpendChan <- bobSpend + + // Here we mock the behavior of a restart. + select { + case aliceNotifier.SpendChan <- bobSpend: + case <-time.After(1 * time.Second): + t.Fatalf("unable to send spend details") + } + + select { + case aliceChainWatcher.BlockbeatChan <- mockBeat: + case <-time.After(time.Second * 1): + t.Fatalf("unable to send blockbeat") + } // We should get a new spend event over the remote unilateral close // event channel. @@ -117,7 +144,7 @@ func TestChainWatcherRemoteUnilateralClosePendingCommit(t *testing.T) { // With the channels created, we'll now create a chain watcher instance // which will be watching for any closes of Alice's channel. - aliceNotifier := &mock.ChainNotifier{ + aliceNotifier := &lnmock.ChainNotifier{ SpendChan: make(chan *chainntnfs.SpendDetail), EpochChan: make(chan *chainntnfs.BlockEpoch), ConfChan: make(chan *chainntnfs.TxConfirmation), @@ -165,7 +192,32 @@ func TestChainWatcherRemoteUnilateralClosePendingCommit(t *testing.T) { SpenderTxHash: &bobTxHash, SpendingTx: bobCommit, } - aliceNotifier.SpendChan <- bobSpend + + // Create a mock blockbeat and send it to Alice's BlockbeatChan. + mockBeat := &chainio.MockBlockbeat{} + + // Mock the logger. We don't care how many times it's called as it's + // not critical. + mockBeat.On("logger").Return(log) + + // Mock a fake block height - this is called based on the debuglevel. + mockBeat.On("Height").Return(int32(1)).Maybe() + + // Mock `NotifyBlockProcessed` to be call once. + mockBeat.On("NotifyBlockProcessed", + nil, aliceChainWatcher.quit).Return().Once() + + select { + case aliceNotifier.SpendChan <- bobSpend: + case <-time.After(1 * time.Second): + t.Fatalf("unable to send spend details") + } + + select { + case aliceChainWatcher.BlockbeatChan <- mockBeat: + case <-time.After(time.Second * 1): + t.Fatalf("unable to send blockbeat") + } // We should get a new spend event over the remote unilateral close // event channel. @@ -279,7 +331,7 @@ func TestChainWatcherDataLossProtect(t *testing.T) { // With the channels created, we'll now create a chain watcher // instance which will be watching for any closes of Alice's // channel. - aliceNotifier := &mock.ChainNotifier{ + aliceNotifier := &lnmock.ChainNotifier{ SpendChan: make(chan *chainntnfs.SpendDetail), EpochChan: make(chan *chainntnfs.BlockEpoch), ConfChan: make(chan *chainntnfs.TxConfirmation), @@ -326,7 +378,34 @@ func TestChainWatcherDataLossProtect(t *testing.T) { SpenderTxHash: &bobTxHash, SpendingTx: bobCommit, } - aliceNotifier.SpendChan <- bobSpend + + // Create a mock blockbeat and send it to Alice's + // BlockbeatChan. + mockBeat := &chainio.MockBlockbeat{} + + // Mock the logger. We don't care how many times it's called as + // it's not critical. + mockBeat.On("logger").Return(log) + + // Mock a fake block height - this is called based on the + // debuglevel. + mockBeat.On("Height").Return(int32(1)).Maybe() + + // Mock `NotifyBlockProcessed` to be call once. + mockBeat.On("NotifyBlockProcessed", + nil, aliceChainWatcher.quit).Return().Once() + + select { + case aliceNotifier.SpendChan <- bobSpend: + case <-time.After(time.Second * 1): + t.Fatalf("failed to send spend notification") + } + + select { + case aliceChainWatcher.BlockbeatChan <- mockBeat: + case <-time.After(time.Second * 1): + t.Fatalf("unable to send blockbeat") + } // We should get a new uni close resolution that indicates we // processed the DLP scenario. @@ -453,7 +532,7 @@ func TestChainWatcherLocalForceCloseDetect(t *testing.T) { // With the channels created, we'll now create a chain watcher // instance which will be watching for any closes of Alice's // channel. - aliceNotifier := &mock.ChainNotifier{ + aliceNotifier := &lnmock.ChainNotifier{ SpendChan: make(chan *chainntnfs.SpendDetail), EpochChan: make(chan *chainntnfs.BlockEpoch), ConfChan: make(chan *chainntnfs.TxConfirmation), @@ -497,7 +576,33 @@ func TestChainWatcherLocalForceCloseDetect(t *testing.T) { SpenderTxHash: &aliceTxHash, SpendingTx: aliceCommit, } - aliceNotifier.SpendChan <- aliceSpend + // Create a mock blockbeat and send it to Alice's + // BlockbeatChan. + mockBeat := &chainio.MockBlockbeat{} + + // Mock the logger. We don't care how many times it's called as + // it's not critical. + mockBeat.On("logger").Return(log) + + // Mock a fake block height - this is called based on the + // debuglevel. + mockBeat.On("Height").Return(int32(1)).Maybe() + + // Mock `NotifyBlockProcessed` to be call once. + mockBeat.On("NotifyBlockProcessed", + nil, aliceChainWatcher.quit).Return().Once() + + select { + case aliceNotifier.SpendChan <- aliceSpend: + case <-time.After(time.Second * 1): + t.Fatalf("unable to send spend notification") + } + + select { + case aliceChainWatcher.BlockbeatChan <- mockBeat: + case <-time.After(time.Second * 1): + t.Fatalf("unable to send blockbeat") + } // We should get a local force close event from Alice as she // should be able to detect the close based on the commitment From 3822c238337e6214538c811e98913d860a97dbf3 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 16 Nov 2024 08:56:48 +0800 Subject: [PATCH 55/92] contractcourt: notify blockbeat for `chainWatcher` We now start notifying the blockbeat from the ChainArbitrator to the chainWatcher. --- contractcourt/chain_arbitrator.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/contractcourt/chain_arbitrator.go b/contractcourt/chain_arbitrator.go index 011b5225cde..00dda99871b 100644 --- a/contractcourt/chain_arbitrator.go +++ b/contractcourt/chain_arbitrator.go @@ -727,19 +727,34 @@ func (c *ChainArbitrator) handleBlockbeat(beat chainio.Blockbeat) { // Create a slice to record active channel arbitrator. channels := make([]chainio.Consumer, 0, len(c.activeChannels)) + watchers := make([]chainio.Consumer, 0, len(c.activeWatchers)) // Copy the active channels to the slice. for _, channel := range c.activeChannels { channels = append(channels, channel) } + for _, watcher := range c.activeWatchers { + watchers = append(watchers, watcher) + } + c.Unlock() + // Iterate all the copied watchers and send the blockbeat to them. + err := chainio.DispatchConcurrent(beat, watchers) + if err != nil { + log.Errorf("Notify blockbeat for chainWatcher failed: %v", err) + } + // Iterate all the copied channels and send the blockbeat to them. // // NOTE: This method will timeout if the processing of blocks of the // subsystems is too long (60s). - err := chainio.DispatchConcurrent(beat, channels) + err = chainio.DispatchConcurrent(beat, channels) + if err != nil { + log.Errorf("Notify blockbeat for ChannelArbitrator failed: %v", + err) + } // Notify the chain arbitrator has processed the block. c.NotifyBlockProcessed(beat, err) From 4d765668cc99cb3f804448e857762963ef761fb6 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Wed, 4 Dec 2024 05:29:55 +0800 Subject: [PATCH 56/92] contractcourt: use close height instead of best height This commit adds the closing height to the logging and fixes a wrong height used in handling the breach event. --- contractcourt/channel_arbitrator.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/contractcourt/channel_arbitrator.go b/contractcourt/channel_arbitrator.go index 3f45c5bfc78..52bf03d2509 100644 --- a/contractcourt/channel_arbitrator.go +++ b/contractcourt/channel_arbitrator.go @@ -2861,7 +2861,8 @@ func (c *ChannelArbitrator) channelAttendant(bestHeight int32, // needed. We'll mark the channel as resolved and exit. case closeInfo := <-c.cfg.ChainEvents.CooperativeClosure: log.Infof("ChannelArbitrator(%v) marking channel "+ - "cooperatively closed", c.cfg.ChanPoint) + "cooperatively closed at height %v", + c.cfg.ChanPoint, closeInfo.CloseHeight) err := c.cfg.MarkChannelClosed( closeInfo.ChannelCloseSummary, @@ -2986,7 +2987,8 @@ func (c *ChannelArbitrator) channelAttendant(bestHeight int32, // all. case uniClosure := <-c.cfg.ChainEvents.RemoteUnilateralClosure: log.Infof("ChannelArbitrator(%v): remote party has "+ - "closed channel out on-chain", c.cfg.ChanPoint) + "force closed channel at height %v", + c.cfg.ChanPoint, uniClosure.SpendingHeight) // If we don't have a self output, and there are no // active HTLC's, then we can immediately mark the @@ -3055,8 +3057,11 @@ func (c *ChannelArbitrator) channelAttendant(bestHeight int32, // anything in particular, so just advance our state and // gracefully exit. case breachInfo := <-c.cfg.ChainEvents.ContractBreach: + closeSummary := &breachInfo.CloseSummary + log.Infof("ChannelArbitrator(%v): remote party has "+ - "breached channel!", c.cfg.ChanPoint) + "breached channel at height %v!", + c.cfg.ChanPoint, closeSummary.CloseHeight) // In the breach case, we'll only have anchor and // breach resolutions. @@ -3088,7 +3093,6 @@ func (c *ChannelArbitrator) channelAttendant(bestHeight int32, // The channel is finally marked pending closed here as // the BreachArbitrator and channel arbitrator have // persisted the relevant states. - closeSummary := &breachInfo.CloseSummary err = c.cfg.MarkChannelClosed( closeSummary, channeldb.ChanStatusRemoteCloseInitiator, @@ -3105,8 +3109,8 @@ func (c *ChannelArbitrator) channelAttendant(bestHeight int32, // We'll advance our state machine until it reaches a // terminal state. _, _, err = c.advanceState( - uint32(bestHeight), breachCloseTrigger, - &breachInfo.CommitSet, + closeSummary.CloseHeight, + breachCloseTrigger, &breachInfo.CommitSet, ) if err != nil { log.Errorf("Unable to advance state: %v", err) From 6eb9bb1ed6e479060ec5ad7d4bfee1bf679fe828 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 14 Nov 2024 02:04:00 +0800 Subject: [PATCH 57/92] multi: add new method `ChainArbitrator.RedispatchBlockbeat` This commit adds a new method to enable us resending the blockbeat in `ChainArbitrator`, which is needed for the channel restore as the chain watcher and channel arbitrator are added after the start of the chain arbitrator. --- chanrestore.go | 13 +++++++++- contractcourt/chain_arbitrator.go | 40 +++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/chanrestore.go b/chanrestore.go index 5b221c105a5..a041f571a8f 100644 --- a/chanrestore.go +++ b/chanrestore.go @@ -8,6 +8,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/chanbackup" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/contractcourt" @@ -286,6 +287,9 @@ func (c *chanDBRestorer) RestoreChansFromSingles(backups ...chanbackup.Single) e ltndLog.Infof("Informing chain watchers of new restored channels") + // Create a slice of channel points. + chanPoints := make([]wire.OutPoint, 0, len(channelShells)) + // Finally, we'll need to inform the chain arbitrator of these new // channels so we'll properly watch for their ultimate closure on chain // and sweep them via the DLP. @@ -294,8 +298,15 @@ func (c *chanDBRestorer) RestoreChansFromSingles(backups ...chanbackup.Single) e if err != nil { return err } + + chanPoints = append( + chanPoints, restoredChannel.Chan.FundingOutpoint, + ) } + // With all the channels restored, we'll now re-send the blockbeat. + c.chainArb.RedispatchBlockbeat(chanPoints) + return nil } @@ -314,7 +325,7 @@ func (s *server) ConnectPeer(nodePub *btcec.PublicKey, addrs []net.Addr) error { // to ensure the new connection is created after this new link/channel // is known. if err := s.DisconnectPeer(nodePub); err != nil { - ltndLog.Infof("Peer(%v) is already connected, proceeding "+ + ltndLog.Infof("Peer(%x) is already connected, proceeding "+ "with chan restore", nodePub.SerializeCompressed()) } diff --git a/contractcourt/chain_arbitrator.go b/contractcourt/chain_arbitrator.go index 00dda99871b..10f7bc21b5f 100644 --- a/contractcourt/chain_arbitrator.go +++ b/contractcourt/chain_arbitrator.go @@ -1371,3 +1371,43 @@ func (c *ChainArbitrator) loadPendingCloseChannels() error { return nil } + +// RedispatchBlockbeat resends the current blockbeat to the channels specified +// by the chanPoints. It is used when a channel is added to the chain +// arbitrator after it has been started, e.g., during the channel restore +// process. +func (c *ChainArbitrator) RedispatchBlockbeat(chanPoints []wire.OutPoint) { + // Get the current blockbeat. + beat := c.beat + + // Prepare two sets of consumers. + channels := make([]chainio.Consumer, 0, len(chanPoints)) + watchers := make([]chainio.Consumer, 0, len(chanPoints)) + + // Read the active channels in a lock. + c.Lock() + for _, op := range chanPoints { + if channel, ok := c.activeChannels[op]; ok { + channels = append(channels, channel) + } + + if watcher, ok := c.activeWatchers[op]; ok { + watchers = append(watchers, watcher) + } + } + c.Unlock() + + // Iterate all the copied watchers and send the blockbeat to them. + err := chainio.DispatchConcurrent(beat, watchers) + if err != nil { + log.Errorf("Notify blockbeat for chainWatcher failed: %v", err) + } + + // Iterate all the copied channels and send the blockbeat to them. + err = chainio.DispatchConcurrent(beat, channels) + if err != nil { + // Shutdown lnd if there's an error processing the block. + log.Errorf("Notify blockbeat for ChannelArbitrator failed: %v", + err) + } +} From c5b3033427289fc124721f1109fc522fd6241592 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 16 Nov 2024 09:02:00 +0800 Subject: [PATCH 58/92] contractcourt: add close event handlers in `ChannelArbitrator` To prepare the next commit where we would handle the event upon receiving a blockbeat. --- contractcourt/channel_arbitrator.go | 455 ++++++++++++++-------------- 1 file changed, 235 insertions(+), 220 deletions(-) diff --git a/contractcourt/channel_arbitrator.go b/contractcourt/channel_arbitrator.go index 52bf03d2509..45c7b5d37fa 100644 --- a/contractcourt/channel_arbitrator.go +++ b/contractcourt/channel_arbitrator.go @@ -2795,8 +2795,6 @@ func (c *ChannelArbitrator) updateActiveHTLCs() { // Nursery for incubation, and ultimate sweeping. // // NOTE: This MUST be run as a goroutine. -// -//nolint:funlen func (c *ChannelArbitrator) channelAttendant(bestHeight int32, commitSet *CommitSet) { @@ -2860,27 +2858,11 @@ func (c *ChannelArbitrator) channelAttendant(bestHeight int32, // We've cooperatively closed the channel, so we're no longer // needed. We'll mark the channel as resolved and exit. case closeInfo := <-c.cfg.ChainEvents.CooperativeClosure: - log.Infof("ChannelArbitrator(%v) marking channel "+ - "cooperatively closed at height %v", - c.cfg.ChanPoint, closeInfo.CloseHeight) - - err := c.cfg.MarkChannelClosed( - closeInfo.ChannelCloseSummary, - channeldb.ChanStatusCoopBroadcasted, - ) + err := c.handleCoopCloseEvent(closeInfo) if err != nil { - log.Errorf("Unable to mark channel closed: "+ - "%v", err) - return - } + log.Errorf("Failed to handle coop close: %v", + err) - // We'll now advance our state machine until it reaches - // a terminal state, and the channel is marked resolved. - _, _, err = c.advanceState( - closeInfo.CloseHeight, coopCloseTrigger, nil, - ) - if err != nil { - log.Errorf("Unable to advance state: %v", err) return } @@ -2893,229 +2875,39 @@ func (c *ChannelArbitrator) channelAttendant(bestHeight int32, c.cfg.ChanPoint) } - closeTx := closeInfo.CloseTx - - resolutions, err := closeInfo.ContractResolutions. - UnwrapOrErr( - fmt.Errorf("resolutions not found"), - ) + err := c.handleLocalForceCloseEvent(closeInfo) if err != nil { - log.Errorf("ChannelArbitrator(%v): unable to "+ - "get resolutions: %v", c.cfg.ChanPoint, - err) - - return - } - - // We make sure that the htlc resolutions are present - // otherwise we would panic dereferencing the pointer. - // - // TODO(ziggie): Refactor ContractResolutions to use - // options. - if resolutions.HtlcResolutions == nil { - log.Errorf("ChannelArbitrator(%v): htlc "+ - "resolutions not found", - c.cfg.ChanPoint) + log.Errorf("Failed to handle local force "+ + "close: %v", err) return } - log.Infof("ChannelArbitrator(%v): local force close "+ - "tx=%v confirmed", c.cfg.ChanPoint, - closeTx.TxHash()) - - contractRes := &ContractResolutions{ - CommitHash: closeTx.TxHash(), - CommitResolution: resolutions.CommitResolution, - HtlcResolutions: *resolutions.HtlcResolutions, - AnchorResolution: resolutions.AnchorResolution, - } - - // When processing a unilateral close event, we'll - // transition to the ContractClosed state. We'll log - // out the set of resolutions such that they are - // available to fetch in that state, we'll also write - // the commit set so we can reconstruct our chain - // actions on restart. - err = c.log.LogContractResolutions(contractRes) - if err != nil { - log.Errorf("Unable to write resolutions: %v", - err) - return - } - err = c.log.InsertConfirmedCommitSet( - &closeInfo.CommitSet, - ) - if err != nil { - log.Errorf("Unable to write commit set: %v", - err) - return - } - - // After the set of resolutions are successfully - // logged, we can safely close the channel. After this - // succeeds we won't be getting chain events anymore, - // so we must make sure we can recover on restart after - // it is marked closed. If the next state transition - // fails, we'll start up in the prior state again, and - // we won't be longer getting chain events. In this - // case we must manually re-trigger the state - // transition into StateContractClosed based on the - // close status of the channel. - err = c.cfg.MarkChannelClosed( - closeInfo.ChannelCloseSummary, - channeldb.ChanStatusLocalCloseInitiator, - ) - if err != nil { - log.Errorf("Unable to mark "+ - "channel closed: %v", err) - return - } - - // We'll now advance our state machine until it reaches - // a terminal state. - _, _, err = c.advanceState( - uint32(closeInfo.SpendingHeight), - localCloseTrigger, &closeInfo.CommitSet, - ) - if err != nil { - log.Errorf("Unable to advance state: %v", err) - } - // The remote party has broadcast the commitment on-chain. // We'll examine our state to determine if we need to act at // all. case uniClosure := <-c.cfg.ChainEvents.RemoteUnilateralClosure: - log.Infof("ChannelArbitrator(%v): remote party has "+ - "force closed channel at height %v", - c.cfg.ChanPoint, uniClosure.SpendingHeight) - - // If we don't have a self output, and there are no - // active HTLC's, then we can immediately mark the - // contract as fully resolved and exit. - contractRes := &ContractResolutions{ - CommitHash: *uniClosure.SpenderTxHash, - CommitResolution: uniClosure.CommitResolution, - HtlcResolutions: *uniClosure.HtlcResolutions, - AnchorResolution: uniClosure.AnchorResolution, - } - - // When processing a unilateral close event, we'll - // transition to the ContractClosed state. We'll log - // out the set of resolutions such that they are - // available to fetch in that state, we'll also write - // the commit set so we can reconstruct our chain - // actions on restart. - err := c.log.LogContractResolutions(contractRes) + err := c.handleRemoteForceCloseEvent(uniClosure) if err != nil { - log.Errorf("Unable to write resolutions: %v", - err) - return - } - err = c.log.InsertConfirmedCommitSet( - &uniClosure.CommitSet, - ) - if err != nil { - log.Errorf("Unable to write commit set: %v", - err) - return - } + log.Errorf("Failed to handle remote force "+ + "close: %v", err) - // After the set of resolutions are successfully - // logged, we can safely close the channel. After this - // succeeds we won't be getting chain events anymore, - // so we must make sure we can recover on restart after - // it is marked closed. If the next state transition - // fails, we'll start up in the prior state again, and - // we won't be longer getting chain events. In this - // case we must manually re-trigger the state - // transition into StateContractClosed based on the - // close status of the channel. - closeSummary := &uniClosure.ChannelCloseSummary - err = c.cfg.MarkChannelClosed( - closeSummary, - channeldb.ChanStatusRemoteCloseInitiator, - ) - if err != nil { - log.Errorf("Unable to mark channel closed: %v", - err) return } - // We'll now advance our state machine until it reaches - // a terminal state. - _, _, err = c.advanceState( - uint32(uniClosure.SpendingHeight), - remoteCloseTrigger, &uniClosure.CommitSet, - ) - if err != nil { - log.Errorf("Unable to advance state: %v", err) - } - // The remote has breached the channel. As this is handled by // the ChainWatcher and BreachArbitrator, we don't have to do // anything in particular, so just advance our state and // gracefully exit. case breachInfo := <-c.cfg.ChainEvents.ContractBreach: - closeSummary := &breachInfo.CloseSummary - - log.Infof("ChannelArbitrator(%v): remote party has "+ - "breached channel at height %v!", - c.cfg.ChanPoint, closeSummary.CloseHeight) - - // In the breach case, we'll only have anchor and - // breach resolutions. - contractRes := &ContractResolutions{ - CommitHash: breachInfo.CommitHash, - BreachResolution: breachInfo.BreachResolution, - AnchorResolution: breachInfo.AnchorResolution, - } - - // We'll transition to the ContractClosed state and log - // the set of resolutions such that they can be turned - // into resolvers later on. We'll also insert the - // CommitSet of the latest set of commitments. - err := c.log.LogContractResolutions(contractRes) - if err != nil { - log.Errorf("Unable to write resolutions: %v", - err) - return - } - err = c.log.InsertConfirmedCommitSet( - &breachInfo.CommitSet, - ) + err := c.handleContractBreach(breachInfo) if err != nil { - log.Errorf("Unable to write commit set: %v", - err) - return - } + log.Errorf("Failed to handle contract breach: "+ + "%v", err) - // The channel is finally marked pending closed here as - // the BreachArbitrator and channel arbitrator have - // persisted the relevant states. - err = c.cfg.MarkChannelClosed( - closeSummary, - channeldb.ChanStatusRemoteCloseInitiator, - ) - if err != nil { - log.Errorf("Unable to mark channel closed: %v", - err) return } - log.Infof("Breached channel=%v marked pending-closed", - breachInfo.BreachResolution.FundingOutPoint) - - // We'll advance our state machine until it reaches a - // terminal state. - _, _, err = c.advanceState( - closeSummary.CloseHeight, - breachCloseTrigger, &breachInfo.CommitSet, - ) - if err != nil { - log.Errorf("Unable to advance state: %v", err) - } - // A new contract has just been resolved, we'll now check our // log to see if all contracts have been resolved. If so, then // we can exit as the contract is fully resolved. @@ -3510,3 +3302,226 @@ func (c *ChannelArbitrator) abandonForwards(htlcs fn.Set[uint64]) error { return nil } + +// handleCoopCloseEvent takes a coop close event from ChainEvents, marks the +// channel as closed and advances the state. +func (c *ChannelArbitrator) handleCoopCloseEvent( + closeInfo *CooperativeCloseInfo) error { + + log.Infof("ChannelArbitrator(%v) marking channel cooperatively closed "+ + "at height %v", c.cfg.ChanPoint, closeInfo.CloseHeight) + + err := c.cfg.MarkChannelClosed( + closeInfo.ChannelCloseSummary, + channeldb.ChanStatusCoopBroadcasted, + ) + if err != nil { + return fmt.Errorf("unable to mark channel closed: %w", err) + } + + // We'll now advance our state machine until it reaches a terminal + // state, and the channel is marked resolved. + _, _, err = c.advanceState(closeInfo.CloseHeight, coopCloseTrigger, nil) + if err != nil { + log.Errorf("Unable to advance state: %v", err) + } + + return nil +} + +// handleLocalForceCloseEvent takes a local force close event from ChainEvents, +// saves the contract resolutions to disk, mark the channel as closed and +// advance the state. +func (c *ChannelArbitrator) handleLocalForceCloseEvent( + closeInfo *LocalUnilateralCloseInfo) error { + + closeTx := closeInfo.CloseTx + + resolutions, err := closeInfo.ContractResolutions. + UnwrapOrErr( + fmt.Errorf("resolutions not found"), + ) + if err != nil { + return fmt.Errorf("unable to get resolutions: %w", err) + } + + // We make sure that the htlc resolutions are present + // otherwise we would panic dereferencing the pointer. + // + // TODO(ziggie): Refactor ContractResolutions to use + // options. + if resolutions.HtlcResolutions == nil { + return fmt.Errorf("htlc resolutions is nil") + } + + log.Infof("ChannelArbitrator(%v): local force close tx=%v confirmed", + c.cfg.ChanPoint, closeTx.TxHash()) + + contractRes := &ContractResolutions{ + CommitHash: closeTx.TxHash(), + CommitResolution: resolutions.CommitResolution, + HtlcResolutions: *resolutions.HtlcResolutions, + AnchorResolution: resolutions.AnchorResolution, + } + + // When processing a unilateral close event, we'll transition to the + // ContractClosed state. We'll log out the set of resolutions such that + // they are available to fetch in that state, we'll also write the + // commit set so we can reconstruct our chain actions on restart. + err = c.log.LogContractResolutions(contractRes) + if err != nil { + return fmt.Errorf("unable to write resolutions: %w", err) + } + + err = c.log.InsertConfirmedCommitSet(&closeInfo.CommitSet) + if err != nil { + return fmt.Errorf("unable to write commit set: %w", err) + } + + // After the set of resolutions are successfully logged, we can safely + // close the channel. After this succeeds we won't be getting chain + // events anymore, so we must make sure we can recover on restart after + // it is marked closed. If the next state transition fails, we'll start + // up in the prior state again, and we won't be longer getting chain + // events. In this case we must manually re-trigger the state + // transition into StateContractClosed based on the close status of the + // channel. + err = c.cfg.MarkChannelClosed( + closeInfo.ChannelCloseSummary, + channeldb.ChanStatusLocalCloseInitiator, + ) + if err != nil { + return fmt.Errorf("unable to mark channel closed: %w", err) + } + + // We'll now advance our state machine until it reaches a terminal + // state. + _, _, err = c.advanceState( + uint32(closeInfo.SpendingHeight), + localCloseTrigger, &closeInfo.CommitSet, + ) + if err != nil { + log.Errorf("Unable to advance state: %v", err) + } + + return nil +} + +// handleRemoteForceCloseEvent takes a remote force close event from +// ChainEvents, saves the contract resolutions to disk, mark the channel as +// closed and advance the state. +func (c *ChannelArbitrator) handleRemoteForceCloseEvent( + closeInfo *RemoteUnilateralCloseInfo) error { + + log.Infof("ChannelArbitrator(%v): remote party has force closed "+ + "channel at height %v", c.cfg.ChanPoint, + closeInfo.SpendingHeight) + + // If we don't have a self output, and there are no active HTLC's, then + // we can immediately mark the contract as fully resolved and exit. + contractRes := &ContractResolutions{ + CommitHash: *closeInfo.SpenderTxHash, + CommitResolution: closeInfo.CommitResolution, + HtlcResolutions: *closeInfo.HtlcResolutions, + AnchorResolution: closeInfo.AnchorResolution, + } + + // When processing a unilateral close event, we'll transition to the + // ContractClosed state. We'll log out the set of resolutions such that + // they are available to fetch in that state, we'll also write the + // commit set so we can reconstruct our chain actions on restart. + err := c.log.LogContractResolutions(contractRes) + if err != nil { + return fmt.Errorf("unable to write resolutions: %w", err) + } + + err = c.log.InsertConfirmedCommitSet(&closeInfo.CommitSet) + if err != nil { + return fmt.Errorf("unable to write commit set: %w", err) + } + + // After the set of resolutions are successfully logged, we can safely + // close the channel. After this succeeds we won't be getting chain + // events anymore, so we must make sure we can recover on restart after + // it is marked closed. If the next state transition fails, we'll start + // up in the prior state again, and we won't be longer getting chain + // events. In this case we must manually re-trigger the state + // transition into StateContractClosed based on the close status of the + // channel. + closeSummary := &closeInfo.ChannelCloseSummary + err = c.cfg.MarkChannelClosed( + closeSummary, + channeldb.ChanStatusRemoteCloseInitiator, + ) + if err != nil { + return fmt.Errorf("unable to mark channel closed: %w", err) + } + + // We'll now advance our state machine until it reaches a terminal + // state. + _, _, err = c.advanceState( + uint32(closeInfo.SpendingHeight), + remoteCloseTrigger, &closeInfo.CommitSet, + ) + if err != nil { + log.Errorf("Unable to advance state: %v", err) + } + + return nil +} + +// handleContractBreach takes a breach close event from ChainEvents, saves the +// contract resolutions to disk, mark the channel as closed and advance the +// state. +func (c *ChannelArbitrator) handleContractBreach( + breachInfo *BreachCloseInfo) error { + + closeSummary := &breachInfo.CloseSummary + + log.Infof("ChannelArbitrator(%v): remote party has breached channel "+ + "at height %v!", c.cfg.ChanPoint, closeSummary.CloseHeight) + + // In the breach case, we'll only have anchor and breach resolutions. + contractRes := &ContractResolutions{ + CommitHash: breachInfo.CommitHash, + BreachResolution: breachInfo.BreachResolution, + AnchorResolution: breachInfo.AnchorResolution, + } + + // We'll transition to the ContractClosed state and log the set of + // resolutions such that they can be turned into resolvers later on. + // We'll also insert the CommitSet of the latest set of commitments. + err := c.log.LogContractResolutions(contractRes) + if err != nil { + return fmt.Errorf("unable to write resolutions: %w", err) + } + + err = c.log.InsertConfirmedCommitSet(&breachInfo.CommitSet) + if err != nil { + return fmt.Errorf("unable to write commit set: %w", err) + } + + // The channel is finally marked pending closed here as the + // BreachArbitrator and channel arbitrator have persisted the relevant + // states. + err = c.cfg.MarkChannelClosed( + closeSummary, channeldb.ChanStatusRemoteCloseInitiator, + ) + if err != nil { + return fmt.Errorf("unable to mark channel closed: %w", err) + } + + log.Infof("Breached channel=%v marked pending-closed", + breachInfo.BreachResolution.FundingOutPoint) + + // We'll advance our state machine until it reaches a terminal state. + _, _, err = c.advanceState( + closeSummary.CloseHeight, breachCloseTrigger, + &breachInfo.CommitSet, + ) + if err != nil { + log.Errorf("Unable to advance state: %v", err) + } + + return nil +} From a6d3a0fa990823d669570fa095ae5bae28e81a50 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 14 Nov 2024 01:39:36 +0800 Subject: [PATCH 59/92] contractcourt: process channel close event on new beat --- contractcourt/briefcase.go | 9 ++++ contractcourt/chain_arbitrator.go | 12 ++--- contractcourt/channel_arbitrator.go | 70 +++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 5 deletions(-) diff --git a/contractcourt/briefcase.go b/contractcourt/briefcase.go index 7d199c5c289..3e58147c615 100644 --- a/contractcourt/briefcase.go +++ b/contractcourt/briefcase.go @@ -249,6 +249,15 @@ func (a ArbitratorState) String() string { } } +// IsContractClosed returns a bool to indicate whether the closing/breaching tx +// has been confirmed onchain. If the state is StateContractClosed, +// StateWaitingFullResolution, or StateFullyResolved, it means the contract has +// been closed and all related contracts have been launched. +func (a ArbitratorState) IsContractClosed() bool { + return a == StateContractClosed || a == StateWaitingFullResolution || + a == StateFullyResolved +} + // resolverType is an enum that enumerates the various types of resolvers. When // writing resolvers to disk, we prepend this to the raw bytes stored. This // allows us to properly decode the resolver into the proper type. diff --git a/contractcourt/chain_arbitrator.go b/contractcourt/chain_arbitrator.go index 10f7bc21b5f..83ae56c7a63 100644 --- a/contractcourt/chain_arbitrator.go +++ b/contractcourt/chain_arbitrator.go @@ -578,9 +578,6 @@ func (c *ChainArbitrator) Start(beat chainio.Blockbeat) error { // Set the current beat. c.beat = beat - log.Infof("ChainArbitrator starting at height %d with budget=[%v]", - &c.cfg.Budget, c.beat.Height()) - // First, we'll fetch all the channels that are still open, in order to // collect them within our set of active contracts. if err := c.loadOpenChannels(); err != nil { @@ -690,6 +687,11 @@ func (c *ChainArbitrator) Start(beat chainio.Blockbeat) error { c.dispatchBlocks() }() + log.Infof("ChainArbitrator starting at height %d with %d chain "+ + "watchers, %d channel arbitrators, and budget config=[%v]", + c.beat.Height(), len(c.activeWatchers), len(c.activeChannels), + &c.cfg.Budget) + // TODO(roasbeef): eventually move all breach watching here return nil @@ -1061,8 +1063,8 @@ func (c *ChainArbitrator) WatchNewChannel(newChan *channeldb.OpenChannel) error chanPoint := newChan.FundingOutpoint - log.Infof("Creating new ChannelArbitrator for ChannelPoint(%v)", - chanPoint) + log.Infof("Creating new chainWatcher and ChannelArbitrator for "+ + "ChannelPoint(%v)", chanPoint) // If we're already watching this channel, then we'll ignore this // request. diff --git a/contractcourt/channel_arbitrator.go b/contractcourt/channel_arbitrator.go index 45c7b5d37fa..f193ee68de6 100644 --- a/contractcourt/channel_arbitrator.go +++ b/contractcourt/channel_arbitrator.go @@ -2992,6 +2992,22 @@ func (c *ChannelArbitrator) handleBlockbeat(beat chainio.Blockbeat) error { // Notify we've processed the block. defer c.NotifyBlockProcessed(beat, nil) + // If the state is StateContractClosed, StateWaitingFullResolution, or + // StateFullyResolved, there's no need to read the close event channel + // and launch the resolvers since the arbitrator can only get to this + // state after processing a previous close event and launched all its + // resolvers. + if c.state.IsContractClosed() { + log.Infof("ChannelArbitrator(%v): skipping launching "+ + "resolvers in state=%v", c.cfg.ChanPoint, c.state) + + return nil + } + + // Perform a non-blocking read on the close events in case the channel + // is closed in this blockbeat. + c.receiveAndProcessCloseEvent() + // Try to advance the state if we are in StateDefault. if c.state == StateDefault { // Now that a new block has arrived, we'll attempt to advance @@ -3010,6 +3026,60 @@ func (c *ChannelArbitrator) handleBlockbeat(beat chainio.Blockbeat) error { return nil } +// receiveAndProcessCloseEvent does a non-blocking read on all the channel +// close event channels. If an event is received, it will be further processed. +func (c *ChannelArbitrator) receiveAndProcessCloseEvent() { + select { + // Received a coop close event, we now mark the channel as resolved and + // exit. + case closeInfo := <-c.cfg.ChainEvents.CooperativeClosure: + err := c.handleCoopCloseEvent(closeInfo) + if err != nil { + log.Errorf("Failed to handle coop close: %v", err) + return + } + + // We have broadcast our commitment, and it is now confirmed onchain. + case closeInfo := <-c.cfg.ChainEvents.LocalUnilateralClosure: + if c.state != StateCommitmentBroadcasted { + log.Errorf("ChannelArbitrator(%v): unexpected "+ + "local on-chain channel close", c.cfg.ChanPoint) + } + + err := c.handleLocalForceCloseEvent(closeInfo) + if err != nil { + log.Errorf("Failed to handle local force close: %v", + err) + + return + } + + // The remote party has broadcast the commitment. We'll examine our + // state to determine if we need to act at all. + case uniClosure := <-c.cfg.ChainEvents.RemoteUnilateralClosure: + err := c.handleRemoteForceCloseEvent(uniClosure) + if err != nil { + log.Errorf("Failed to handle remote force close: %v", + err) + + return + } + + // The remote has breached the channel! We now launch the breach + // contract resolvers. + case breachInfo := <-c.cfg.ChainEvents.ContractBreach: + err := c.handleContractBreach(breachInfo) + if err != nil { + log.Errorf("Failed to handle contract breach: %v", err) + return + } + + default: + log.Infof("ChannelArbitrator(%v) no close event", + c.cfg.ChanPoint) + } +} + // Name returns a human-readable string for this subsystem. // // NOTE: Part of chainio.Consumer interface. From ea7d6a509bc2b07e96d7e8a04b930b0a1253d149 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 22 Nov 2024 22:28:51 +0800 Subject: [PATCH 60/92] contractcourt: register conf notification once and cancel when confirmed --- contractcourt/chain_watcher.go | 60 +++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/contractcourt/chain_watcher.go b/contractcourt/chain_watcher.go index cd8d6f3542a..fef70b945a1 100644 --- a/contractcourt/chain_watcher.go +++ b/contractcourt/chain_watcher.go @@ -224,13 +224,6 @@ type chainWatcher struct { // the current state number on the commitment transactions. stateHintObfuscator [lnwallet.StateHintSize]byte - // fundingPkScript is the pkScript of the funding output. - fundingPkScript []byte - - // heightHint is the height hint used to checkpoint scans on chain for - // conf/spend events. - heightHint uint32 - // All the fields below are protected by this mutex. sync.Mutex @@ -245,6 +238,18 @@ type chainWatcher struct { // fundingSpendNtfn is the spending notification subscription for the // funding outpoint. fundingSpendNtfn *chainntnfs.SpendEvent + + // fundingConfirmedNtfn is the confirmation notification subscription + // for the funding outpoint. This is only created if the channel is + // both taproot and pending confirmation. + // + // For taproot pkscripts, `RegisterSpendNtfn` will only notify on the + // outpoint being spent and not the outpoint+pkscript due to + // `ComputePkScript` being unable to compute the pkscript if a key + // spend is used. We need to add a `RegisterConfirmationsNtfn` here to + // ensure that the outpoint+pkscript pair is confirmed before calling + // `RegisterSpendNtfn`. + fundingConfirmedNtfn *chainntnfs.ConfirmationEvent } // newChainWatcher returns a new instance of a chainWatcher for a channel given @@ -292,11 +297,24 @@ func newChainWatcher(cfg chainWatcherConfig) (*chainWatcher, error) { stateHintObfuscator: stateHint, quit: make(chan struct{}), clientSubscriptions: make(map[uint64]*ChainEventSubscription), - fundingPkScript: fundingPkScript, - heightHint: heightHint, fundingSpendNtfn: spendNtfn, } + // If this is a pending taproot channel, we need to register for a + // confirmation notification of the funding tx. Check the docs in + // `fundingConfirmedNtfn` for details. + if c.cfg.chanState.IsPending && c.cfg.chanState.ChanType.IsTaproot() { + confNtfn, err := cfg.notifier.RegisterConfirmationsNtfn( + &chanState.FundingOutpoint.Hash, fundingPkScript, 1, + heightHint, + ) + if err != nil { + return nil, err + } + + c.fundingConfirmedNtfn = confNtfn + } + // Mount the block consumer. c.BeatConsumer = chainio.NewBeatConsumer(c.quit, c.Name()) @@ -1472,28 +1490,24 @@ func (c *chainWatcher) checkFundingSpend() *chainntnfs.SpendDetail { // chanPointConfirmed checks whether the given channel point has confirmed. // This is used to ensure that the funding output has confirmed on chain before // we proceed with the rest of the close observer logic for taproot channels. +// Check the docs in `fundingConfirmedNtfn` for details. func (c *chainWatcher) chanPointConfirmed() bool { op := c.cfg.chanState.FundingOutpoint - confNtfn, err := c.cfg.notifier.RegisterConfirmationsNtfn( - &op.Hash, c.fundingPkScript, 1, c.heightHint, - ) - if err != nil { - log.Errorf("Unable to register for conf: %v", err) - - return false - } select { - case _, ok := <-confNtfn.Confirmed: + case _, ok := <-c.fundingConfirmedNtfn.Confirmed: // If the channel was closed, then this means that the notifier // exited, so we will as well. if !ok { -// Check the docs in `fundingConfirmedNtfn` for details. return false } log.Debugf("Taproot ChannelPoint(%v) confirmed", op) + // The channel point has confirmed on chain. We now cancel the + // subscription. + c.fundingConfirmedNtfn.Cancel() + return true default: @@ -1510,9 +1524,11 @@ func (c *chainWatcher) handleBlockbeat(beat chainio.Blockbeat) { // Notify the chain watcher has processed the block. defer c.NotifyBlockProcessed(beat, nil) - // If this is a taproot channel, before we proceed, we want to ensure - // that the expected funding output has confirmed on chain. - if c.cfg.chanState.IsPending && c.cfg.chanState.ChanType.IsTaproot() { + // If we have a fundingConfirmedNtfn, it means this is a taproot + // channel that is pending, before we proceed, we want to ensure that + // the expected funding output has confirmed on chain. Check the docs + // in `fundingConfirmedNtfn` for details. + if c.fundingConfirmedNtfn != nil { // If the funding output hasn't confirmed in this block, we // will check it again in the next block. if !c.chanPointConfirmed() { From cc60d2b41c507c4166e4242b9f660260f2115ddd Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Mon, 25 Nov 2024 14:04:18 +0800 Subject: [PATCH 61/92] chainntnfs: skip dispatched conf details We need to check `dispatched` before sending conf details, otherwise the channel `ntfn.Event.Confirmed` will be blocking, which is the leftover from #9275. --- chainntnfs/txnotifier.go | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/chainntnfs/txnotifier.go b/chainntnfs/txnotifier.go index 71b9c929df4..12366abf955 100644 --- a/chainntnfs/txnotifier.go +++ b/chainntnfs/txnotifier.go @@ -1757,10 +1757,6 @@ func (n *TxNotifier) NotifyHeight(height uint32) error { for ntfn := range n.ntfnsByConfirmHeight[height] { confSet := n.confNotifications[ntfn.ConfRequest] - Log.Debugf("Dispatching %v confirmation notification for "+ - "conf_id=%v, %v", ntfn.NumConfirmations, ntfn.ConfID, - ntfn.ConfRequest) - // The default notification we assigned above includes the // block along with the rest of the details. However not all // clients want the block, so we make a copy here w/o the block @@ -1770,6 +1766,20 @@ func (n *TxNotifier) NotifyHeight(height uint32) error { confDetails.Block = nil } + // If the `confDetails` has already been sent before, we'll + // skip it and continue processing the next one. + if ntfn.dispatched { + Log.Debugf("Skipped dispatched conf details for "+ + "request %v conf_id=%v", ntfn.ConfRequest, + ntfn.ConfID) + + continue + } + + Log.Debugf("Dispatching %v confirmation notification for "+ + "conf_id=%v, %v", ntfn.NumConfirmations, ntfn.ConfID, + ntfn.ConfRequest) + select { case ntfn.Event.Confirmed <- &confDetails: ntfn.dispatched = true From bd88948264a203a8b309765db59e3ab7cd931627 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Mon, 25 Nov 2024 14:19:23 +0800 Subject: [PATCH 62/92] docs: add release notes for `blockbeat` series --- docs/release-notes/release-notes-0.19.0.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/release-notes/release-notes-0.19.0.md b/docs/release-notes/release-notes-0.19.0.md index 43ed7ebbcdc..f9491658f88 100644 --- a/docs/release-notes/release-notes-0.19.0.md +++ b/docs/release-notes/release-notes-0.19.0.md @@ -124,6 +124,18 @@ * LND updates channel.backup file at shutdown time. +* A new subsystem `chainio` is + [introduced](https://github.com/lightningnetwork/lnd/pull/9315) to make sure + the subsystems are in sync with their current best block. Previously, when + resolving a force close channel, the sweeping of HTLCs may be delayed for one + or two blocks due to block heights not in sync in the relevant subsystems + (`ChainArbitrator`, `UtxoSweeper` and `TxPublisher`), causing a slight + inaccuracy when deciding the sweeping feerate and urgency. With `chainio`, + this is now fixed as these subsystems now share the same view on the best + block. Check + [here](https://github.com/lightningnetwork/lnd/blob/master/chainio/README.md) + to learn more. + ## RPC Updates * Some RPCs that previously just returned an empty response message now at least From fecd5ac735d3169e11e888d5472ac44fc3ea3f67 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Wed, 18 Dec 2024 19:33:41 +0800 Subject: [PATCH 63/92] contractcourt: make sure `launchResolvers` is called on new blockbeat This is an oversight from addressing this commment: https://github.com/lightningnetwork/lnd/pull/9277#discussion_r1882410396 where we should focus on skipping the close events but not the resolvers. --- contractcourt/channel_arbitrator.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/contractcourt/channel_arbitrator.go b/contractcourt/channel_arbitrator.go index f193ee68de6..3b1c82ce5f1 100644 --- a/contractcourt/channel_arbitrator.go +++ b/contractcourt/channel_arbitrator.go @@ -2994,12 +2994,18 @@ func (c *ChannelArbitrator) handleBlockbeat(beat chainio.Blockbeat) error { // If the state is StateContractClosed, StateWaitingFullResolution, or // StateFullyResolved, there's no need to read the close event channel - // and launch the resolvers since the arbitrator can only get to this - // state after processing a previous close event and launched all its - // resolvers. + // since the arbitrator can only get to this state after processing a + // previous close event and launched all its resolvers. if c.state.IsContractClosed() { - log.Infof("ChannelArbitrator(%v): skipping launching "+ - "resolvers in state=%v", c.cfg.ChanPoint, c.state) + log.Infof("ChannelArbitrator(%v): skipping reading close "+ + "events in state=%v", c.cfg.ChanPoint, c.state) + + // Launch all active resolvers when a new blockbeat is + // received, even when the contract is closed, we still need + // this as the resolvers may transform into new ones. For + // already launched resolvers this will be NOOP as they track + // their own `launched` states. + c.launchResolvers() return nil } From 4806b2fda7aff7d6c698c88c19bc57238d09701e Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 18 Oct 2024 05:35:00 +0800 Subject: [PATCH 64/92] multi: optimize loggings around changes from `blockbeat` --- chainio/consumer.go | 6 +-- chainio/dispatcher.go | 5 +++ contractcourt/channel_arbitrator.go | 8 ++-- .../htlc_outgoing_contest_resolver.go | 5 +-- lnutils/log.go | 13 +++++- sweep/fee_bumper.go | 5 ++- sweep/sweeper.go | 45 ++++++++++--------- sweep/txgenerator.go | 3 +- sweep/txgenerator_test.go | 10 +++-- 9 files changed, 62 insertions(+), 38 deletions(-) diff --git a/chainio/consumer.go b/chainio/consumer.go index a9ec25745ba..eb059a62450 100644 --- a/chainio/consumer.go +++ b/chainio/consumer.go @@ -79,7 +79,7 @@ func (b *BeatConsumer) ProcessBlock(beat Blockbeat) error { // `beat.NotifyBlockProcessed` to send the error back here. select { case err := <-b.errChan: - beat.logger().Debugf("[%s] processed beat: err=%v", b.name, err) + beat.logger().Tracef("[%s] processed beat: err=%v", b.name, err) return err @@ -99,11 +99,11 @@ func (b *BeatConsumer) ProcessBlock(beat Blockbeat) error { // processing the block. func (b *BeatConsumer) NotifyBlockProcessed(beat Blockbeat, err error) { // Update the current height. - beat.logger().Debugf("[%s]: notifying beat processed", b.name) + beat.logger().Tracef("[%s]: notifying beat processed", b.name) select { case b.errChan <- err: - beat.logger().Debugf("[%s]: notified beat processed, err=%v", + beat.logger().Tracef("[%s]: notified beat processed, err=%v", b.name, err) case <-b.quit: diff --git a/chainio/dispatcher.go b/chainio/dispatcher.go index 87bc21fbaac..269bb1892c1 100644 --- a/chainio/dispatcher.go +++ b/chainio/dispatcher.go @@ -9,6 +9,7 @@ import ( "github.com/btcsuite/btclog/v2" "github.com/lightningnetwork/lnd/chainntnfs" + "github.com/lightningnetwork/lnd/lnutils" "golang.org/x/sync/errgroup" ) @@ -136,6 +137,10 @@ func (b *BlockbeatDispatcher) dispatchBlocks( return } + // Log a separator so it's easier to identify when a + // new block arrives for subsystems. + clog.Debugf("%v", lnutils.NewSeparatorClosure()) + clog.Infof("Received new block %v at height %d, "+ "notifying consumers...", blockEpoch.Hash, blockEpoch.Height) diff --git a/contractcourt/channel_arbitrator.go b/contractcourt/channel_arbitrator.go index 3b1c82ce5f1..cfefea4573f 100644 --- a/contractcourt/channel_arbitrator.go +++ b/contractcourt/channel_arbitrator.go @@ -479,7 +479,7 @@ func (c *ChannelArbitrator) Start(state *chanArbStartState, } } - log.Debugf("Starting ChannelArbitrator(%v), htlc_set=%v, state=%v", + log.Tracef("Starting ChannelArbitrator(%v), htlc_set=%v, state=%v", c.cfg.ChanPoint, lnutils.SpewLogClosure(c.activeHTLCs), state.currentState) @@ -2618,14 +2618,14 @@ func (c *ChannelArbitrator) replaceResolver(oldResolver, func (c *ChannelArbitrator) resolveContract(currentContract ContractResolver) { defer c.wg.Done() - log.Debugf("ChannelArbitrator(%v): attempting to resolve %T", + log.Tracef("ChannelArbitrator(%v): attempting to resolve %T", c.cfg.ChanPoint, currentContract) // Until the contract is fully resolved, we'll continue to iteratively // resolve the contract one step at a time. for !currentContract.IsResolved() { - log.Debugf("ChannelArbitrator(%v): contract %T not yet resolved", - c.cfg.ChanPoint, currentContract) + log.Tracef("ChannelArbitrator(%v): contract %T not yet "+ + "resolved", c.cfg.ChanPoint, currentContract) select { diff --git a/contractcourt/htlc_outgoing_contest_resolver.go b/contractcourt/htlc_outgoing_contest_resolver.go index b66a3fdf0b3..9e94587cccb 100644 --- a/contractcourt/htlc_outgoing_contest_resolver.go +++ b/contractcourt/htlc_outgoing_contest_resolver.go @@ -165,9 +165,8 @@ func (h *htlcOutgoingContestResolver) Resolve() (ContractResolver, error) { if newHeight >= expiry-1 { h.log.Infof("HTLC about to expire "+ "(height=%v, expiry=%v), transforming "+ - "into timeout resolver", h, - h.htlcResolution.ClaimOutpoint, - newHeight, h.htlcResolution.Expiry) + "into timeout resolver", newHeight, + h.htlcResolution.Expiry) return h.htlcTimeoutResolver, nil } diff --git a/lnutils/log.go b/lnutils/log.go index a32738bdf4b..a588bf3ce34 100644 --- a/lnutils/log.go +++ b/lnutils/log.go @@ -1,6 +1,10 @@ package lnutils -import "github.com/davecgh/go-spew/spew" +import ( + "strings" + + "github.com/davecgh/go-spew/spew" +) // LogClosure is used to provide a closure over expensive logging operations so // don't have to be performed when the logging level doesn't warrant it. @@ -25,3 +29,10 @@ func SpewLogClosure(a any) LogClosure { return spew.Sdump(a) } } + +// NewSeparatorClosure returns a new closure that logs a separator line. +func NewSeparatorClosure() LogClosure { + return func() string { + return strings.Repeat("=", 80) + } +} diff --git a/sweep/fee_bumper.go b/sweep/fee_bumper.go index a43f8758508..57f9f6e3037 100644 --- a/sweep/fee_bumper.go +++ b/sweep/fee_bumper.go @@ -1588,7 +1588,10 @@ func prepareSweepTx(inputs []input.Input, changePkScript lnwallet.AddrWithKey, // Check if the lock time has reached if lt > uint32(currentHeight) { - return 0, noChange, noLocktime, ErrLocktimeImmature + return 0, noChange, noLocktime, + fmt.Errorf("%w: current height is %v, "+ + "locktime is %v", ErrLocktimeImmature, + currentHeight, lt) } // If another input commits to a different locktime, they diff --git a/sweep/sweeper.go b/sweep/sweeper.go index d49f104af06..88404e52a83 100644 --- a/sweep/sweeper.go +++ b/sweep/sweeper.go @@ -229,7 +229,7 @@ func (p *SweeperInput) isMature(currentHeight uint32) (bool, uint32) { locktime, _ := p.RequiredLockTime() if currentHeight < locktime { log.Debugf("Input %v has locktime=%v, current height is %v", - p.OutPoint(), locktime, currentHeight) + p, locktime, currentHeight) return false, locktime } @@ -243,8 +243,7 @@ func (p *SweeperInput) isMature(currentHeight uint32) (bool, uint32) { locktime = p.BlocksToMaturity() + p.HeightHint() if currentHeight+1 < locktime { log.Debugf("Input %v has CSV expiry=%v, current height is %v, "+ - "skipped sweeping", p.OutPoint(), locktime, - currentHeight) + "skipped sweeping", p, locktime, currentHeight) return false, locktime } @@ -255,6 +254,21 @@ func (p *SweeperInput) isMature(currentHeight uint32) (bool, uint32) { // InputsMap is a type alias for a set of pending inputs. type InputsMap = map[wire.OutPoint]*SweeperInput +// inputsMapToString returns a human readable interpretation of the pending +// inputs. +func inputsMapToString(inputs InputsMap) string { + if len(inputs) == 0 { + return "" + } + + inps := make([]input.Input, 0, len(inputs)) + for _, in := range inputs { + inps = append(inps, in) + } + + return "\n" + inputTypeSummary(inps) +} + // pendingSweepsReq is an internal message we'll use to represent an external // caller's intent to retrieve all of the pending inputs the UtxoSweeper is // attempting to sweep. @@ -700,17 +714,10 @@ func (s *UtxoSweeper) collector() { inputs := s.updateSweeperInputs() log.Debugf("Received new block: height=%v, attempt "+ - "sweeping %d inputs:\n%s", - s.currentHeight, len(inputs), + "sweeping %d inputs:%s", s.currentHeight, + len(inputs), lnutils.NewLogClosure(func() string { - inps := make( - []input.Input, 0, len(inputs), - ) - for _, in := range inputs { - inps = append(inps, in) - } - - return inputTypeSummary(inps) + return inputsMapToString(inputs) })) // Attempt to sweep any pending inputs. @@ -1244,10 +1251,10 @@ func (s *UtxoSweeper) handleNewInput(input *sweepInputMessage) error { log.Tracef("input %v, state=%v, added to inputs", outpoint, pi.state) log.Infof("Registered sweep request at block %d: out_point=%v, "+ - "witness_type=%v, amount=%v, deadline=%d, params=(%v)", - s.currentHeight, pi.OutPoint(), pi.WitnessType(), + "witness_type=%v, amount=%v, deadline=%d, state=%v, "+ + "params=(%v)", s.currentHeight, pi.OutPoint(), pi.WitnessType(), btcutil.Amount(pi.SignDesc().Output.Value), pi.DeadlineHeight, - pi.params) + pi.state, pi.params) // Start watching for spend of this input, either by us or the remote // party. @@ -1523,12 +1530,8 @@ func (s *UtxoSweeper) updateSweeperInputs() InputsMap { // If the input has a locktime that's not yet reached, we will // skip this input and wait for the locktime to be reached. - mature, locktime := input.isMature(uint32(s.currentHeight)) + mature, _ := input.isMature(uint32(s.currentHeight)) if !mature { - log.Debugf("Skipping input %v due to locktime=%v not "+ - "reached, current height is %v", op, locktime, - s.currentHeight) - continue } diff --git a/sweep/txgenerator.go b/sweep/txgenerator.go index 993ee9e59dd..995949b15aa 100644 --- a/sweep/txgenerator.go +++ b/sweep/txgenerator.go @@ -315,5 +315,6 @@ func inputTypeSummary(inputs []input.Input) string { part := fmt.Sprintf("%v (%v)", i.OutPoint(), i.WitnessType()) parts = append(parts, part) } - return strings.Join(parts, ", ") + + return strings.Join(parts, "\n") } diff --git a/sweep/txgenerator_test.go b/sweep/txgenerator_test.go index 71477bd6ec0..3f2051646a4 100644 --- a/sweep/txgenerator_test.go +++ b/sweep/txgenerator_test.go @@ -16,10 +16,12 @@ var ( input.HtlcOfferedRemoteTimeout, input.WitnessKeyHash, } - expectedWeight = int64(1460) - expectedSummary = "0000000000000000000000000000000000000000000000000000000000000000:10 (CommitmentTimeLock), " + - "0000000000000000000000000000000000000000000000000000000000000001:11 (HtlcAcceptedSuccessSecondLevel), " + - "0000000000000000000000000000000000000000000000000000000000000002:12 (HtlcOfferedRemoteTimeout), " + + expectedWeight = int64(1460) + + //nolint:ll + expectedSummary = "0000000000000000000000000000000000000000000000000000000000000000:10 (CommitmentTimeLock)\n" + + "0000000000000000000000000000000000000000000000000000000000000001:11 (HtlcAcceptedSuccessSecondLevel)\n" + + "0000000000000000000000000000000000000000000000000000000000000002:12 (HtlcOfferedRemoteTimeout)\n" + "0000000000000000000000000000000000000000000000000000000000000003:13 (WitnessKeyHash)" ) From 40ac04a25454ac1a077934fa84ccacd9d7e9a6ff Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 24 Oct 2024 19:50:47 +0800 Subject: [PATCH 65/92] lntest+itest: fix `testSweepCPFPAnchorOutgoingTimeout` --- itest/lnd_sweep_test.go | 121 ++++++++++++++++-------------------- lntest/harness_assertion.go | 45 ++++++++++++++ 2 files changed, 100 insertions(+), 66 deletions(-) diff --git a/itest/lnd_sweep_test.go b/itest/lnd_sweep_test.go index 158e8768f94..881918a72d9 100644 --- a/itest/lnd_sweep_test.go +++ b/itest/lnd_sweep_test.go @@ -61,10 +61,7 @@ func testSweepCPFPAnchorOutgoingTimeout(ht *lntest.HarnessTest) { // Set up the fee estimator to return the testing fee rate when the // conf target is the deadline. - // - // TODO(yy): switch to conf when `blockbeat` is in place. - // ht.SetFeeEstimateWithConf(startFeeRateAnchor, deadlineDeltaAnchor) - ht.SetFeeEstimate(startFeeRateAnchor) + ht.SetFeeEstimateWithConf(startFeeRateAnchor, deadlineDeltaAnchor) // htlcValue is the outgoing HTLC's value. htlcValue := invoiceAmt @@ -171,16 +168,20 @@ func testSweepCPFPAnchorOutgoingTimeout(ht *lntest.HarnessTest) { )) ht.MineEmptyBlocks(int(numBlocks)) - // Assert Bob's force closing tx has been broadcast. - closeTxid := ht.AssertNumTxsInMempool(1)[0] + // Assert Bob's force closing tx has been broadcast. We should see two + // txns in the mempool: + // 1. Bob's force closing tx. + // 2. Bob's anchor sweeping tx CPFPing the force close tx. + _, sweepTx := ht.AssertForceCloseAndAnchorTxnsInMempool() // Remember the force close height so we can calculate the deadline // height. forceCloseHeight := ht.CurrentHeight() - // Bob should have two pending sweeps, + var anchorSweep *walletrpc.PendingSweep + + // Bob should have one pending sweep, // - anchor sweeping from his local commitment. - // - anchor sweeping from his remote commitment (invalid). // // TODO(yy): consider only sweeping the anchor from the local // commitment. Previously we would sweep up to three versions of @@ -189,34 +190,22 @@ func testSweepCPFPAnchorOutgoingTimeout(ht *lntest.HarnessTest) { // their commitment tx and replaces ours. With the new fee bumping, we // should be safe to only sweep our local anchor since we RBF it on // every new block, which destroys the remote's ability to pin us. - sweeps := ht.AssertNumPendingSweeps(bob, 2) + expectedNumSweeps := 1 - // The two anchor sweeping should have the same deadline height. - deadlineHeight := forceCloseHeight + deadlineDeltaAnchor - require.Equal(ht, deadlineHeight, sweeps[0].DeadlineHeight) - require.Equal(ht, deadlineHeight, sweeps[1].DeadlineHeight) - - // Remember the deadline height for the CPFP anchor. - anchorDeadline := sweeps[0].DeadlineHeight - - // Mine a block so Bob's force closing tx stays in the mempool, which - // also triggers the CPFP anchor sweep. - ht.MineEmptyBlocks(1) + // For neutrino backend, Bob would have two anchor sweeps - one from + // the local and the other from the remote. + if ht.IsNeutrinoBackend() { + expectedNumSweeps = 2 + } - // Bob should still have two pending sweeps, - // - anchor sweeping from his local commitment. - // - anchor sweeping from his remote commitment (invalid). - ht.AssertNumPendingSweeps(bob, 2) + anchorSweep = ht.AssertNumPendingSweeps(bob, expectedNumSweeps)[0] - // We now check the expected fee and fee rate are used for Bob's anchor - // sweeping tx. - // - // We should see Bob's anchor sweeping tx triggered by the above - // block, along with his force close tx. - txns := ht.GetNumTxsFromMempool(2) + // The anchor sweeping should have the expected deadline height. + deadlineHeight := forceCloseHeight + deadlineDeltaAnchor + require.Equal(ht, deadlineHeight, anchorSweep.DeadlineHeight) - // Find the sweeping tx. - sweepTx := ht.FindSweepingTxns(txns, 1, closeTxid)[0] + // Remember the deadline height for the CPFP anchor. + anchorDeadline := anchorSweep.DeadlineHeight // Get the weight for Bob's anchor sweeping tx. txWeight := ht.CalculateTxWeight(sweepTx) @@ -228,11 +217,10 @@ func testSweepCPFPAnchorOutgoingTimeout(ht *lntest.HarnessTest) { fee := uint64(ht.CalculateTxFee(sweepTx)) feeRate := uint64(ht.CalculateTxFeeRate(sweepTx)) - // feeFuncWidth is the width of the fee function. By the time we got - // here, we've already mined one block, and the fee function maxes - // out one block before the deadline, so the width is the original - // deadline minus 2. - feeFuncWidth := deadlineDeltaAnchor - 2 + // feeFuncWidth is the width of the fee function. The fee function + // maxes out one block before the deadline, so the width is the + // original deadline minus 1. + feeFuncWidth := deadlineDeltaAnchor - 1 // Calculate the expected delta increased per block. feeDelta := (cpfpBudget - startFeeAnchor).MulF64( @@ -258,20 +246,27 @@ func testSweepCPFPAnchorOutgoingTimeout(ht *lntest.HarnessTest) { // Bob's fee bumper should increase its fees. ht.MineEmptyBlocks(1) - // Bob should still have two pending sweeps, - // - anchor sweeping from his local commitment. - // - anchor sweeping from his remote commitment (invalid). - ht.AssertNumPendingSweeps(bob, 2) - - // Make sure Bob's old sweeping tx has been removed from the - // mempool. - ht.AssertTxNotInMempool(sweepTx.TxHash()) + // Bob should still have the anchor sweeping from his local + // commitment. His anchor sweeping from his remote commitment + // is invalid and should be removed. + ht.AssertNumPendingSweeps(bob, expectedNumSweeps) // We expect to see two txns in the mempool, // - Bob's force close tx. // - Bob's anchor sweep tx. ht.AssertNumTxsInMempool(2) + // Make sure Bob's old sweeping tx has been removed from the + // mempool. + ht.AssertTxNotInMempool(sweepTx.TxHash()) + + // Assert the two txns are still in the mempool and grab the + // sweeping tx. + // + // NOTE: must call it again after `AssertTxNotInMempool` to + // make sure we get the replaced tx. + _, sweepTx = ht.AssertForceCloseAndAnchorTxnsInMempool() + // We expect the fees to increase by i*delta. expectedFee := startFeeAnchor + feeDelta.MulF64(float64(i)) expectedFeeRate := chainfee.NewSatPerKWeight( @@ -280,11 +275,6 @@ func testSweepCPFPAnchorOutgoingTimeout(ht *lntest.HarnessTest) { // We should see Bob's anchor sweeping tx being fee bumped // since it's not confirmed, along with his force close tx. - txns = ht.GetNumTxsFromMempool(2) - - // Find the sweeping tx. - sweepTx = ht.FindSweepingTxns(txns, 1, closeTxid)[0] - // Calculate the fee rate of Bob's new sweeping tx. feeRate = uint64(ht.CalculateTxFeeRate(sweepTx)) @@ -292,9 +282,9 @@ func testSweepCPFPAnchorOutgoingTimeout(ht *lntest.HarnessTest) { fee = uint64(ht.CalculateTxFee(sweepTx)) ht.Logf("Bob(position=%v): txWeight=%v, expected: [fee=%d, "+ - "feerate=%v], got: [fee=%v, feerate=%v]", + "feerate=%v], got: [fee=%v, feerate=%v] in tx %v", feeFuncWidth-i, txWeight, expectedFee, - expectedFeeRate, fee, feeRate) + expectedFeeRate, fee, feeRate, sweepTx.TxHash()) // Assert Bob's tx has the expected fee and fee rate. require.InEpsilonf(ht, uint64(expectedFee), fee, 0.01, @@ -314,22 +304,23 @@ func testSweepCPFPAnchorOutgoingTimeout(ht *lntest.HarnessTest) { // Mine one more block, we'd use up all the CPFP budget. ht.MineEmptyBlocks(1) + // We expect to see two txns in the mempool, + // - Bob's force close tx. + // - Bob's anchor sweep tx. + ht.AssertNumTxsInMempool(2) + // Make sure Bob's old sweeping tx has been removed from the mempool. ht.AssertTxNotInMempool(sweepTx.TxHash()) // Get the last sweeping tx - we should see two txns here, Bob's anchor // sweeping tx and his force close tx. - txns = ht.GetNumTxsFromMempool(2) - - // Find the sweeping tx. - sweepTx = ht.FindSweepingTxns(txns, 1, closeTxid)[0] - - // Calculate the fee of Bob's new sweeping tx. - fee = uint64(ht.CalculateTxFee(sweepTx)) + // + // NOTE: must call it again after `AssertTxNotInMempool` to make sure + // we get the replaced tx. + _, sweepTx = ht.AssertForceCloseAndAnchorTxnsInMempool() - // Assert the budget is now used up. - require.InEpsilonf(ht, uint64(cpfpBudget), fee, 0.01, "want %d, got %d", - cpfpBudget, fee) + // Bob should have the anchor sweeping from his local commitment. + ht.AssertNumPendingSweeps(bob, expectedNumSweeps) // Mine one more block. Since Bob's budget has been used up, there // won't be any more sweeping attempts. We now assert this by checking @@ -340,10 +331,7 @@ func testSweepCPFPAnchorOutgoingTimeout(ht *lntest.HarnessTest) { // // We expect two txns here, one for the anchor sweeping, the other for // the force close tx. - txns = ht.GetNumTxsFromMempool(2) - - // Find the sweeping tx. - currentSweepTx := ht.FindSweepingTxns(txns, 1, closeTxid)[0] + _, currentSweepTx := ht.AssertForceCloseAndAnchorTxnsInMempool() // Assert the anchor sweep tx stays unchanged. require.Equal(ht, sweepTx.TxHash(), currentSweepTx.TxHash()) @@ -357,6 +345,7 @@ func testSweepCPFPAnchorOutgoingTimeout(ht *lntest.HarnessTest) { // the HTLC sweeping behaviors so we just perform a simple check and // exit the test. ht.AssertNumPendingSweeps(bob, 1) + ht.MineBlocksAndAssertNumTxes(1, 1) // Finally, clean the mempool for the next test. ht.CleanShutDown() diff --git a/lntest/harness_assertion.go b/lntest/harness_assertion.go index 11cbefdd5c0..b60985527b7 100644 --- a/lntest/harness_assertion.go +++ b/lntest/harness_assertion.go @@ -2786,3 +2786,48 @@ func (h *HarnessTest) FindSweepingTxns(txns []*wire.MsgTx, return sweepTxns } + +// AssertForceCloseAndAnchorTxnsInMempool asserts that the force close and +// anchor sweep txns are found in the mempool and returns the force close tx +// and the anchor sweep tx. +func (h *HarnessTest) AssertForceCloseAndAnchorTxnsInMempool() (*wire.MsgTx, + *wire.MsgTx) { + + // Assert there are two txns in the mempool. + txns := h.GetNumTxsFromMempool(2) + + // isParentAndChild checks whether there is an input used in the + // assumed child tx by checking every input's previous outpoint against + // the assumed parentTxid. + isParentAndChild := func(parent, child *wire.MsgTx) bool { + parentTxid := parent.TxHash() + + for _, inp := range child.TxIn { + if inp.PreviousOutPoint.Hash == parentTxid { + // Found a match, this is indeed the anchor + // sweeping tx so we return it here. + return true + } + } + + return false + } + + switch { + // Assume the first one is the closing tx and the second one is the + // anchor sweeping tx. + case isParentAndChild(txns[0], txns[1]): + return txns[0], txns[1] + + // Assume the first one is the anchor sweeping tx and the second one is + // the closing tx. + case isParentAndChild(txns[1], txns[0]): + return txns[1], txns[0] + + // Unrelated txns found, fail the test. + default: + h.Fatalf("the two txns not related: %v", txns) + + return nil, nil + } +} From cacf222e119b4fc61ecb380da7edac9eeedbcd47 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 24 Oct 2024 19:51:46 +0800 Subject: [PATCH 66/92] itest: fix `testSweepCPFPAnchorIncomingTimeout` --- itest/lnd_sweep_test.go | 102 +++++++++++++++++----------------------- 1 file changed, 43 insertions(+), 59 deletions(-) diff --git a/itest/lnd_sweep_test.go b/itest/lnd_sweep_test.go index 881918a72d9..f1e055162d3 100644 --- a/itest/lnd_sweep_test.go +++ b/itest/lnd_sweep_test.go @@ -393,10 +393,7 @@ func testSweepCPFPAnchorIncomingTimeout(ht *lntest.HarnessTest) { // Set up the fee estimator to return the testing fee rate when the // conf target is the deadline. - // - // TODO(yy): switch to conf when `blockbeat` is in place. - // ht.SetFeeEstimateWithConf(startFeeRateAnchor, deadlineDeltaAnchor) - ht.SetFeeEstimate(startFeeRateAnchor) + ht.SetFeeEstimateWithConf(startFeeRateAnchor, deadlineDeltaAnchor) // Create a preimage, that will be held by Carol. var preimage lntypes.Preimage @@ -513,40 +510,30 @@ func testSweepCPFPAnchorIncomingTimeout(ht *lntest.HarnessTest) { numBlocks := forceCloseHeight - currentHeight ht.MineEmptyBlocks(int(numBlocks)) - // Assert Bob's force closing tx has been broadcast. - closeTxid := ht.AssertNumTxsInMempool(1)[0] + // Assert Bob's force closing tx has been broadcast. We should see two + // txns in the mempool: + // 1. Bob's force closing tx. + // 2. Bob's anchor sweeping tx CPFPing the force close tx. + _, sweepTx := ht.AssertForceCloseAndAnchorTxnsInMempool() - // Bob should have two pending sweeps, + // Bob should have one pending sweep, // - anchor sweeping from his local commitment. - // - anchor sweeping from his remote commitment (invalid). - sweeps := ht.AssertNumPendingSweeps(bob, 2) - - // The two anchor sweeping should have the same deadline height. - deadlineHeight := forceCloseHeight + deadlineDeltaAnchor - require.Equal(ht, deadlineHeight, sweeps[0].DeadlineHeight) - require.Equal(ht, deadlineHeight, sweeps[1].DeadlineHeight) - - // Remember the deadline height for the CPFP anchor. - anchorDeadline := sweeps[0].DeadlineHeight + expectedNumSweeps := 1 - // Mine a block so Bob's force closing tx stays in the mempool, which - // also triggers the CPFP anchor sweep. - ht.MineEmptyBlocks(1) + // For neutrino backend, Bob would have two anchor sweeps - one from + // the local and the other from the remote. + if ht.IsNeutrinoBackend() { + expectedNumSweeps = 2 + } - // Bob should still have two pending sweeps, - // - anchor sweeping from his local commitment. - // - anchor sweeping from his remote commitment (invalid). - ht.AssertNumPendingSweeps(bob, 2) + anchorSweep := ht.AssertNumPendingSweeps(bob, expectedNumSweeps)[0] - // We now check the expected fee and fee rate are used for Bob's anchor - // sweeping tx. - // - // We should see Bob's anchor sweeping tx triggered by the above - // block, along with his force close tx. - txns := ht.GetNumTxsFromMempool(2) + // The anchor sweeping should have the expected deadline height. + deadlineHeight := forceCloseHeight + deadlineDeltaAnchor + require.Equal(ht, deadlineHeight, anchorSweep.DeadlineHeight) - // Find the sweeping tx. - sweepTx := ht.FindSweepingTxns(txns, 1, closeTxid)[0] + // Remember the deadline height for the CPFP anchor. + anchorDeadline := anchorSweep.DeadlineHeight // Get the weight for Bob's anchor sweeping tx. txWeight := ht.CalculateTxWeight(sweepTx) @@ -558,11 +545,10 @@ func testSweepCPFPAnchorIncomingTimeout(ht *lntest.HarnessTest) { fee := uint64(ht.CalculateTxFee(sweepTx)) feeRate := uint64(ht.CalculateTxFeeRate(sweepTx)) - // feeFuncWidth is the width of the fee function. By the time we got - // here, we've already mined one block, and the fee function maxes - // out one block before the deadline, so the width is the original - // deadline minus 2. - feeFuncWidth := deadlineDeltaAnchor - 2 + // feeFuncWidth is the width of the fee function. The fee function + // maxes out one block before the deadline, so the width is the + // original deadline minus 1. + feeFuncWidth := deadlineDeltaAnchor - 1 // Calculate the expected delta increased per block. feeDelta := (cpfpBudget - startFeeAnchor).MulF64( @@ -588,10 +574,15 @@ func testSweepCPFPAnchorIncomingTimeout(ht *lntest.HarnessTest) { // Bob's fee bumper should increase its fees. ht.MineEmptyBlocks(1) - // Bob should still have two pending sweeps, - // - anchor sweeping from his local commitment. - // - anchor sweeping from his remote commitment (invalid). - ht.AssertNumPendingSweeps(bob, 2) + // Bob should still have the anchor sweeping from his local + // commitment. His anchor sweeping from his remote commitment + // is invalid and should be removed. + ht.AssertNumPendingSweeps(bob, expectedNumSweeps) + + // We expect to see two txns in the mempool, + // - Bob's force close tx. + // - Bob's anchor sweep tx. + ht.AssertNumTxsInMempool(2) // Make sure Bob's old sweeping tx has been removed from the // mempool. @@ -600,7 +591,7 @@ func testSweepCPFPAnchorIncomingTimeout(ht *lntest.HarnessTest) { // We expect to see two txns in the mempool, // - Bob's force close tx. // - Bob's anchor sweep tx. - ht.AssertNumTxsInMempool(2) + _, sweepTx = ht.AssertForceCloseAndAnchorTxnsInMempool() // We expect the fees to increase by i*delta. expectedFee := startFeeAnchor + feeDelta.MulF64(float64(i)) @@ -608,13 +599,6 @@ func testSweepCPFPAnchorIncomingTimeout(ht *lntest.HarnessTest) { expectedFee, txWeight, ) - // We should see Bob's anchor sweeping tx being fee bumped - // since it's not confirmed, along with his force close tx. - txns = ht.GetNumTxsFromMempool(2) - - // Find the sweeping tx. - sweepTx = ht.FindSweepingTxns(txns, 1, closeTxid)[0] - // Calculate the fee rate of Bob's new sweeping tx. feeRate = uint64(ht.CalculateTxFeeRate(sweepTx)) @@ -622,9 +606,9 @@ func testSweepCPFPAnchorIncomingTimeout(ht *lntest.HarnessTest) { fee = uint64(ht.CalculateTxFee(sweepTx)) ht.Logf("Bob(position=%v): txWeight=%v, expected: [fee=%d, "+ - "feerate=%v], got: [fee=%v, feerate=%v]", + "feerate=%v], got: [fee=%v, feerate=%v] in tx %v", feeFuncWidth-i, txWeight, expectedFee, - expectedFeeRate, fee, feeRate) + expectedFeeRate, fee, feeRate, sweepTx.TxHash()) // Assert Bob's tx has the expected fee and fee rate. require.InEpsilonf(ht, uint64(expectedFee), fee, 0.01, @@ -644,15 +628,17 @@ func testSweepCPFPAnchorIncomingTimeout(ht *lntest.HarnessTest) { // Mine one more block, we'd use up all the CPFP budget. ht.MineEmptyBlocks(1) + // We expect to see two txns in the mempool, + // - Bob's force close tx. + // - Bob's anchor sweep tx. + ht.AssertNumTxsInMempool(2) + // Make sure Bob's old sweeping tx has been removed from the mempool. ht.AssertTxNotInMempool(sweepTx.TxHash()) // Get the last sweeping tx - we should see two txns here, Bob's anchor // sweeping tx and his force close tx. - txns = ht.GetNumTxsFromMempool(2) - - // Find the sweeping tx. - sweepTx = ht.FindSweepingTxns(txns, 1, closeTxid)[0] + _, sweepTx = ht.AssertForceCloseAndAnchorTxnsInMempool() // Calculate the fee of Bob's new sweeping tx. fee = uint64(ht.CalculateTxFee(sweepTx)) @@ -670,10 +656,7 @@ func testSweepCPFPAnchorIncomingTimeout(ht *lntest.HarnessTest) { // // We expect two txns here, one for the anchor sweeping, the other for // the force close tx. - txns = ht.GetNumTxsFromMempool(2) - - // Find the sweeping tx. - currentSweepTx := ht.FindSweepingTxns(txns, 1, closeTxid)[0] + _, currentSweepTx := ht.AssertForceCloseAndAnchorTxnsInMempool() // Assert the anchor sweep tx stays unchanged. require.Equal(ht, sweepTx.TxHash(), currentSweepTx.TxHash()) @@ -687,6 +670,7 @@ func testSweepCPFPAnchorIncomingTimeout(ht *lntest.HarnessTest) { // the HTLC sweeping behaviors so we just perform a simple check and // exit the test. ht.AssertNumPendingSweeps(bob, 1) + ht.MineBlocksAndAssertNumTxes(1, 1) // Finally, clean the mempool for the next test. ht.CleanShutDown() From d260a87f3bd465a90f055a0249102d39b144aec9 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 24 Oct 2024 19:52:30 +0800 Subject: [PATCH 67/92] itest: fix `testSweepHTLCs` --- itest/lnd_sweep_test.go | 88 ++++++++++++++++++++++------------------- 1 file changed, 47 insertions(+), 41 deletions(-) diff --git a/itest/lnd_sweep_test.go b/itest/lnd_sweep_test.go index f1e055162d3..83633e39b9c 100644 --- a/itest/lnd_sweep_test.go +++ b/itest/lnd_sweep_test.go @@ -708,9 +708,9 @@ func testSweepHTLCs(ht *lntest.HarnessTest) { cltvDelta := routing.MinCLTVDelta // Start tracking the deadline delta of Bob's HTLCs. We need one block - // for the CSV lock, and another block to trigger the sweeper to sweep. - outgoingHTLCDeadline := int32(cltvDelta - 2) - incomingHTLCDeadline := int32(lncfg.DefaultIncomingBroadcastDelta - 2) + // to trigger the sweeper to sweep. + outgoingHTLCDeadline := int32(cltvDelta - 1) + incomingHTLCDeadline := int32(lncfg.DefaultIncomingBroadcastDelta - 1) // startFeeRate1 and startFeeRate2 are returned by the fee estimator in // sat/kw. They will be used as the starting fee rate for the linear @@ -867,34 +867,35 @@ func testSweepHTLCs(ht *lntest.HarnessTest) { // Bob should now have two pending sweeps, one for the anchor on the // local commitment, the other on the remote commitment. - ht.AssertNumPendingSweeps(bob, 2) + expectedNumSweeps := 1 + + // For neutrino backend, we expect the anchor output from his remote + // commitment to be present. + if ht.IsNeutrinoBackend() { + expectedNumSweeps = 2 + } + + ht.AssertNumPendingSweeps(bob, expectedNumSweeps) - // Assert Bob's force closing tx has been broadcast. - ht.AssertNumTxsInMempool(1) + // We expect to see two txns in the mempool: + // 1. Bob's force closing tx. + // 2. Bob's anchor CPFP sweeping tx. + ht.AssertNumTxsInMempool(2) - // Mine the force close tx, which triggers Bob's contractcourt to offer - // his outgoing HTLC to his sweeper. + // Mine the force close tx and CPFP sweeping tx, which triggers Bob's + // contractcourt to offer his outgoing HTLC to his sweeper. // // NOTE: HTLC outputs are only offered to sweeper when the force close // tx is confirmed and the CSV has reached. - ht.MineBlocksAndAssertNumTxes(1, 1) - - // Update the blocks left till Bob force closes Alice->Bob. - blocksTillIncomingSweep-- - - // Bob should have two pending sweeps, one for the anchor sweeping, the - // other for the outgoing HTLC. - ht.AssertNumPendingSweeps(bob, 2) - - // Mine one block to confirm Bob's anchor sweeping tx, which will - // trigger his sweeper to publish the HTLC sweeping tx. - ht.MineBlocksAndAssertNumTxes(1, 1) + ht.MineBlocksAndAssertNumTxes(1, 2) // Update the blocks left till Bob force closes Alice->Bob. blocksTillIncomingSweep-- - // Bob should now have one sweep and one sweeping tx in the mempool. + // Bob should have one pending sweep for the outgoing HTLC. ht.AssertNumPendingSweeps(bob, 1) + + // Bob should have one sweeping tx in the mempool. outgoingSweep := ht.GetNumTxsFromMempool(1)[0] // Check the shape of the sweeping tx - we expect it to be @@ -918,8 +919,8 @@ func testSweepHTLCs(ht *lntest.HarnessTest) { // Assert the initial sweeping tx is using the start fee rate. outgoingStartFeeRate := ht.CalculateTxFeeRate(outgoingSweep) require.InEpsilonf(ht, uint64(startFeeRate1), - uint64(outgoingStartFeeRate), 0.01, "want %d, got %d", - startFeeRate1, outgoingStartFeeRate) + uint64(outgoingStartFeeRate), 0.01, "want %d, got %d in tx=%v", + startFeeRate1, outgoingStartFeeRate, outgoingSweep.TxHash()) // Now the start fee rate is checked, we can calculate the fee rate // delta. @@ -944,13 +945,12 @@ func testSweepHTLCs(ht *lntest.HarnessTest) { ) ht.Logf("Bob's %s HTLC (deadline=%v): txWeight=%v, want "+ - "feerate=%v, got feerate=%v, delta=%v", desc, + "feerate=%v, got feerate=%v, delta=%v in tx %v", desc, deadline-position, txSize, expectedFeeRate, - feeRate, delta) + feeRate, delta, sweepTx.TxHash()) require.InEpsilonf(ht, uint64(expectedFeeRate), uint64(feeRate), - 0.01, "want %v, got %v in tx=%v", expectedFeeRate, - feeRate, sweepTx.TxHash()) + 0.01, "want %v, got %v", expectedFeeRate, feeRate) } // We now mine enough blocks to trigger Bob to force close channel @@ -992,22 +992,34 @@ func testSweepHTLCs(ht *lntest.HarnessTest) { // Update Bob's fee function position. outgoingFuncPosition++ - // Bob should now have three pending sweeps: + // Bob should now have two pending sweeps: // 1. the outgoing HTLC output. // 2. the anchor output from his local commitment. - // 3. the anchor output from his remote commitment. - ht.AssertNumPendingSweeps(bob, 3) + expectedNumSweeps = 2 - // We should see two txns in the mempool: + // For neutrino backend, we expect the anchor output from his remote + // commitment to be present. + if ht.IsNeutrinoBackend() { + expectedNumSweeps = 3 + } + + ht.AssertNumPendingSweeps(bob, expectedNumSweeps) + + // We should see three txns in the mempool: // 1. Bob's outgoing HTLC sweeping tx. // 2. Bob's force close tx for Alice->Bob. - txns := ht.GetNumTxsFromMempool(2) + // 3. Bob's anchor CPFP sweeping tx for Alice->Bob. + txns := ht.GetNumTxsFromMempool(3) // Find the force close tx - we expect it to have a single input. closeTx := txns[0] if len(closeTx.TxIn) != 1 { closeTx = txns[1] } + if len(closeTx.TxIn) != 1 { + closeTx = txns[2] + } + require.Len(ht, closeTx.TxIn, 1) // We don't care the behavior of the anchor sweep in this test, so we // mine the force close tx to trigger Bob's contractcourt to offer his @@ -1023,13 +1035,6 @@ func testSweepHTLCs(ht *lntest.HarnessTest) { // 3. the anchor sweeping on Alice-> Bob. ht.AssertNumPendingSweeps(bob, 3) - // Mine one block, which will trigger his sweeper to publish his - // incoming HTLC sweeping tx. - ht.MineEmptyBlocks(1) - - // Update the fee function's positions. - outgoingFuncPosition++ - // We should see three txns in the mempool: // 1. the outgoing HTLC sweeping tx. // 2. the incoming HTLC sweeping tx. @@ -1197,8 +1202,9 @@ func testSweepHTLCs(ht *lntest.HarnessTest) { // Test: // 1. Alice's anchor sweeping is not attempted, instead, it should be swept // together with her to_local output using the no deadline path. -// 2. Bob would also sweep his anchor and to_local outputs in a single -// sweeping tx using the no deadline path. +// 2. Bob would also sweep his anchor and to_local outputs separately due to +// they have different deadline heights, which means only the to_local +// sweeping tx will succeed as the anchor sweeping is not economical. // 3. Both Alice and Bob's RBF attempts are using the fee rates calculated // from the deadline and budget. // 4. Wallet UTXOs requirements are met - neither Alice nor Bob needs wallet From 1aeea8a90f04f78b51003250a6870f10d81ddc9a Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 24 Oct 2024 19:52:55 +0800 Subject: [PATCH 68/92] itest: fix `testSweepCommitOutputAndAnchor` --- itest/lnd_sweep_test.go | 359 ++++++++-------------------------------- 1 file changed, 73 insertions(+), 286 deletions(-) diff --git a/itest/lnd_sweep_test.go b/itest/lnd_sweep_test.go index 83633e39b9c..4bcca07704c 100644 --- a/itest/lnd_sweep_test.go +++ b/itest/lnd_sweep_test.go @@ -2,7 +2,6 @@ package itest import ( "fmt" - "math" "time" "github.com/btcsuite/btcd/btcutil" @@ -1217,10 +1216,20 @@ func testSweepCommitOutputAndAnchor(ht *lntest.HarnessTest) { // config. deadline := uint32(1000) - // The actual deadline used by the fee function will be one block off - // from the deadline configured as we require one block to be mined to - // trigger the sweep. - deadlineA, deadlineB := deadline-1, deadline-1 + // deadlineA is the deadline used for Alice, since her commit output is + // offered to the sweeper at CSV-1. With a deadline of 1000, her actual + // width of her fee func is CSV+1000-1. Given we are using a CSV of 2 + // here, her fee func deadline then becomes 1001. + deadlineA := deadline + 1 + + // deadlineB is the deadline used for Bob, the actual deadline used by + // the fee function will be one block off from the deadline configured + // as we require one block to be mined to trigger the sweep. In + // addition, when sweeping his to_local output from Alice's commit tx, + // because of CSV of 2, the starting height will be + // "force_close_height+2", which means when the sweep request is + // received by the sweeper, the actual deadline delta is "deadline+1". + deadlineB := deadline + 1 // startFeeRate is returned by the fee estimator in sat/kw. This // will be used as the starting fee rate for the linear fee func used @@ -1231,7 +1240,7 @@ func testSweepCommitOutputAndAnchor(ht *lntest.HarnessTest) { // Set up the fee estimator to return the testing fee rate when the // conf target is the deadline. - ht.SetFeeEstimateWithConf(startFeeRate, deadlineA) + ht.SetFeeEstimateWithConf(startFeeRate, deadlineB) // toLocalCSV is the CSV delay for Alice's to_local output. We use a // small value to save us from mining blocks. @@ -1239,25 +1248,7 @@ func testSweepCommitOutputAndAnchor(ht *lntest.HarnessTest) { // NOTE: once the force close tx is confirmed, we expect anchor // sweeping starts. Then two more block later the commit output // sweeping starts. - // - // NOTE: The CSV value is chosen to be 3 instead of 2, to reduce the - // possibility of flakes as there is a race between the two goroutines: - // G1 - Alice's sweeper receives the commit output. - // G2 - Alice's sweeper receives the new block mined. - // G1 is triggered by the same block being received by Alice's - // contractcourt, deciding the commit output is mature and offering it - // to her sweeper. Normally, we'd expect G2 to be finished before G1 - // because it's the same block processed by both contractcourt and - // sweeper. However, if G2 is delayed (maybe the sweeper is slow in - // finishing its previous round), G1 may finish before G2. This will - // cause the sweeper to add the commit output to its pending inputs, - // and once G2 fires, it will then start sweeping this output, - // resulting a valid sweep tx being created using her commit and anchor - // outputs. - // - // TODO(yy): fix the above issue by making sure subsystems share the - // same view on current block height. - toLocalCSV := 3 + toLocalCSV := 2 // htlcAmt is the amount of the HTLC in sats, this should be Alice's // to_remote amount that goes to Bob. @@ -1356,155 +1347,41 @@ func testSweepCommitOutputAndAnchor(ht *lntest.HarnessTest) { // - commit sweeping from the to_remote on Alice's commit tx. ht.AssertNumPendingSweeps(bob, 2) + // Bob's sweeper should have broadcast the commit output sweeping tx. + // At the block which mined the force close tx, Bob's `chainWatcher` + // will process the blockbeat first, which sends a signal to his + // `ChainArbitrator` to launch the resolvers. Once launched, the sweep + // requests will be sent to the sweeper. Finally, when the sweeper + // receives this blockbeat, it will create the sweeping tx and publish + // it. + ht.AssertNumTxsInMempool(1) + // Mine one more empty block should trigger Bob's sweeping. Since we - // use a CSV of 3, this means Alice's to_local output is one block away - // from being mature. + // use a CSV of 2, this means Alice's to_local output is now mature. ht.MineEmptyBlocks(1) - // We expect to see one sweeping tx in the mempool: - // - Alice's anchor sweeping tx must have been failed due to the fee - // rate chosen in this test - the anchor sweep tx has no output. - // - Bob's sweeping tx, which sweeps both his anchor and commit outputs. - bobSweepTx := ht.GetNumTxsFromMempool(1)[0] - // We expect two pending sweeps for Bob - anchor and commit outputs. - pendingSweepBob := ht.AssertNumPendingSweeps(bob, 2)[0] - - // The sweeper may be one block behind contractcourt, so we double - // check the actual deadline. - // - // TODO(yy): assert they are equal once blocks are synced via - // `blockbeat`. - currentHeight := int32(ht.CurrentHeight()) - actualDeadline := int32(pendingSweepBob.DeadlineHeight) - currentHeight - if actualDeadline != int32(deadlineB) { - ht.Logf("!!! Found unsynced block between sweeper and "+ - "contractcourt, expected deadline=%v, got=%v", - deadlineB, actualDeadline) - - deadlineB = uint32(actualDeadline) - } - - // Alice should still have one pending sweep - the anchor output. - ht.AssertNumPendingSweeps(alice, 1) - - // We now check Bob's sweeping tx. - // - // Bob's sweeping tx should have 2 inputs, one from his commit output, - // the other from his anchor output. - require.Len(ht, bobSweepTx.TxIn, 2) - - // Because Bob is sweeping without deadline pressure, the starting fee - // rate should be the min relay fee rate. - bobStartFeeRate := ht.CalculateTxFeeRate(bobSweepTx) - require.InEpsilonf(ht, uint64(chainfee.FeePerKwFloor), - uint64(bobStartFeeRate), 0.01, "want %v, got %v", - chainfee.FeePerKwFloor, bobStartFeeRate) - - // With Bob's starting fee rate being validated, we now calculate his - // ending fee rate and fee rate delta. - // - // Bob sweeps two inputs - anchor and commit, so the starting budget - // should come from the sum of these two. - bobValue := btcutil.Amount(bobToLocal + 330) - bobBudget := bobValue.MulF64(contractcourt.DefaultBudgetRatio) - - // Calculate the ending fee rate and fee rate delta used in his fee - // function. - bobTxWeight := ht.CalculateTxWeight(bobSweepTx) - bobEndingFeeRate := chainfee.NewSatPerKWeight(bobBudget, bobTxWeight) - bobFeeRateDelta := (bobEndingFeeRate - bobStartFeeRate) / - chainfee.SatPerKWeight(deadlineB-1) - - // Mine an empty block, which should trigger Alice's contractcourt to - // offer her commit output to the sweeper. - ht.MineEmptyBlocks(1) - - // Alice should have both anchor and commit as the pending sweep - // requests. - aliceSweeps := ht.AssertNumPendingSweeps(alice, 2) - aliceAnchor, aliceCommit := aliceSweeps[0], aliceSweeps[1] - if aliceAnchor.AmountSat > aliceCommit.AmountSat { - aliceAnchor, aliceCommit = aliceCommit, aliceAnchor - } - - // The sweeper may be one block behind contractcourt, so we double - // check the actual deadline. - // - // TODO(yy): assert they are equal once blocks are synced via - // `blockbeat`. - currentHeight = int32(ht.CurrentHeight()) - actualDeadline = int32(aliceCommit.DeadlineHeight) - currentHeight - if actualDeadline != int32(deadlineA) { - ht.Logf("!!! Found unsynced block between Alice's sweeper and "+ - "contractcourt, expected deadline=%v, got=%v", - deadlineA, actualDeadline) - - deadlineA = uint32(actualDeadline) - } - - // We now wait for 30 seconds to overcome the flake - there's a block - // race between contractcourt and sweeper, causing the sweep to be - // broadcast earlier. - // - // TODO(yy): remove this once `blockbeat` is in place. - aliceStartPosition := 0 - var aliceFirstSweepTx *wire.MsgTx - err := wait.NoError(func() error { - mem := ht.GetRawMempool() - if len(mem) != 2 { - return fmt.Errorf("want 2, got %v in mempool: %v", - len(mem), mem) - } - - // If there are two txns, it means Alice's sweep tx has been - // created and published. - aliceStartPosition = 1 - - txns := ht.GetNumTxsFromMempool(2) - aliceFirstSweepTx = txns[0] - - // Reassign if the second tx is larger. - if txns[1].TxOut[0].Value > aliceFirstSweepTx.TxOut[0].Value { - aliceFirstSweepTx = txns[1] - } - - return nil - }, wait.DefaultTimeout) - ht.Logf("Checking mempool got: %v", err) - - // Mine an empty block, which should trigger Alice's sweeper to publish - // her commit sweep along with her anchor output. - ht.MineEmptyBlocks(1) + ht.AssertNumPendingSweeps(bob, 2) - // If Alice has already published her initial sweep tx, the above mined - // block would trigger an RBF. We now need to assert the mempool has - // removed the replaced tx. - if aliceFirstSweepTx != nil { - ht.AssertTxNotInMempool(aliceFirstSweepTx.TxHash()) - } + // We expect two pending sweeps for Alice - anchor and commit outputs. + ht.AssertNumPendingSweeps(alice, 2) // We also remember the positions of fee functions used by Alice and // Bob. They will be used to calculate the expected fee rates later. - // - // Alice's sweeping tx has just been created, so she is at the starting - // position. For Bob, due to the above mined blocks, his fee function - // is now at position 2. - alicePosition, bobPosition := uint32(aliceStartPosition), uint32(2) + alicePosition, bobPosition := uint32(0), uint32(1) // We should see two txns in the mempool: // - Alice's sweeping tx, which sweeps her commit output at the // starting fee rate - Alice's anchor output won't be swept with her // commit output together because they have different deadlines. - // - Bob's previous sweeping tx, which sweeps both his anchor and - // commit outputs, at the starting fee rate. + // - Bob's previous sweeping tx, which sweeps his and commit outputs, + // at the starting fee rate. txns := ht.GetNumTxsFromMempool(2) // Assume the first tx is Alice's sweeping tx, if the second tx has a // larger output value, then that's Alice's as her to_local value is // much gearter. - aliceSweepTx := txns[0] - bobSweepTx = txns[1] + aliceSweepTx, bobSweepTx := txns[0], txns[1] // Swap them if bobSweepTx is smaller. if bobSweepTx.TxOut[0].Value > aliceSweepTx.TxOut[0].Value { @@ -1518,20 +1395,6 @@ func testSweepCommitOutputAndAnchor(ht *lntest.HarnessTest) { require.Len(ht, aliceSweepTx.TxIn, 1) require.Len(ht, aliceSweepTx.TxOut, 1) - // We now check Alice's sweeping tx to see if it's already published. - // - // TODO(yy): remove this check once we have better block control. - aliceSweeps = ht.AssertNumPendingSweeps(alice, 2) - aliceCommit = aliceSweeps[0] - if aliceCommit.AmountSat < aliceSweeps[1].AmountSat { - aliceCommit = aliceSweeps[1] - } - if aliceCommit.BroadcastAttempts > 1 { - ht.Logf("!!! Alice's commit sweep has already been broadcast, "+ - "broadcast_attempts=%v", aliceCommit.BroadcastAttempts) - alicePosition = aliceCommit.BroadcastAttempts - } - // Alice's sweeping tx should use the min relay fee rate as there's no // deadline pressure. aliceStartingFeeRate := chainfee.FeePerKwFloor @@ -1546,7 +1409,7 @@ func testSweepCommitOutputAndAnchor(ht *lntest.HarnessTest) { aliceTxWeight := uint64(ht.CalculateTxWeight(aliceSweepTx)) aliceEndingFeeRate := sweep.DefaultMaxFeeRate.FeePerKWeight() aliceFeeRateDelta := (aliceEndingFeeRate - aliceStartingFeeRate) / - chainfee.SatPerKWeight(deadlineA-1) + chainfee.SatPerKWeight(deadlineA) aliceFeeRate := ht.CalculateTxFeeRate(aliceSweepTx) expectedFeeRateAlice := aliceStartingFeeRate + @@ -1555,119 +1418,41 @@ func testSweepCommitOutputAndAnchor(ht *lntest.HarnessTest) { uint64(aliceFeeRate), 0.02, "want %v, got %v", expectedFeeRateAlice, aliceFeeRate) - // We now check Bob' sweeping tx. - // - // The above mined block will trigger Bob's sweeper to RBF his previous - // sweeping tx, which will fail due to RBF rule#4 - the additional fees - // paid are not sufficient. This happens as our default incremental - // relay fee rate is 1 sat/vb, with the tx size of 771 weight units, or - // 192 vbytes, we need to pay at least 192 sats more to be able to RBF. - // However, since Bob's budget delta is (100_000 + 330) * 0.5 / 1008 = - // 49.77 sats, it means Bob can only perform a successful RBF every 4 - // blocks. - // - // Assert Bob's sweeping tx is not RBFed. - bobFeeRate := ht.CalculateTxFeeRate(bobSweepTx) - expectedFeeRateBob := bobStartFeeRate - require.InEpsilonf(ht, uint64(expectedFeeRateBob), uint64(bobFeeRate), - 0.01, "want %d, got %d", expectedFeeRateBob, bobFeeRate) - - // reloclateAlicePosition is a temp hack to find the actual fee - // function position used for Alice. Due to block sync issue among the - // subsystems, we can end up having this situation: - // - sweeper is at block 2, starts sweeping an input with deadline 100. - // - fee bumper is at block 1, and thinks the conf target is 99. - // - new block 3 arrives, the func now is at position 2. + // We now check Bob's sweeping tx. // - // TODO(yy): fix it using `blockbeat`. - reloclateAlicePosition := func() { - // Mine an empty block to trigger the possible RBF attempts. - ht.MineEmptyBlocks(1) - - // Increase the positions for both fee functions. - alicePosition++ - bobPosition++ - - // We expect two pending sweeps for both nodes as we are mining - // empty blocks. - ht.AssertNumPendingSweeps(alice, 2) - ht.AssertNumPendingSweeps(bob, 2) - - // We expect to see both Alice's and Bob's sweeping txns in the - // mempool. - ht.AssertNumTxsInMempool(2) - - // Make sure Alice's old sweeping tx has been removed from the - // mempool. - ht.AssertTxNotInMempool(aliceSweepTx.TxHash()) - - // We should see two txns in the mempool: - // - Alice's sweeping tx, which sweeps both her anchor and - // commit outputs, using the increased fee rate. - // - Bob's previous sweeping tx, which sweeps both his anchor - // and commit outputs, at the possible increased fee rate. - txns = ht.GetNumTxsFromMempool(2) - - // Assume the first tx is Alice's sweeping tx, if the second tx - // has a larger output value, then that's Alice's as her - // to_local value is much gearter. - aliceSweepTx = txns[0] - bobSweepTx = txns[1] - - // Swap them if bobSweepTx is smaller. - if bobSweepTx.TxOut[0].Value > aliceSweepTx.TxOut[0].Value { - aliceSweepTx, bobSweepTx = bobSweepTx, aliceSweepTx - } - - // Alice's sweeping tx should be increased. - aliceFeeRate := ht.CalculateTxFeeRate(aliceSweepTx) - expectedFeeRate := aliceStartingFeeRate + - aliceFeeRateDelta*chainfee.SatPerKWeight(alicePosition) + // Bob's sweeping tx should have one input, which is his commit output. + // His anchor output won't be swept due to it being uneconomical. + require.Len(ht, bobSweepTx.TxIn, 1, "tx=%v", bobSweepTx.TxHash()) - ht.Logf("Alice(deadline=%v): txWeight=%v, want feerate=%v, "+ - "got feerate=%v, delta=%v", deadlineA-alicePosition, - aliceTxWeight, expectedFeeRate, aliceFeeRate, - aliceFeeRateDelta) - - nextPosition := alicePosition + 1 - nextFeeRate := aliceStartingFeeRate + - aliceFeeRateDelta*chainfee.SatPerKWeight(nextPosition) - - // Calculate the distances. - delta := math.Abs(float64(aliceFeeRate - expectedFeeRate)) - deltaNext := math.Abs(float64(aliceFeeRate - nextFeeRate)) - - // Exit early if the first distance is smaller - it means we - // are at the right fee func position. - if delta < deltaNext { - require.InEpsilonf(ht, uint64(expectedFeeRate), - uint64(aliceFeeRate), 0.02, "want %v, got %v "+ - "in tx=%v", expectedFeeRate, - aliceFeeRate, aliceSweepTx.TxHash()) - - return - } - - alicePosition++ - ht.Logf("Jump position for Alice(deadline=%v): txWeight=%v, "+ - "want feerate=%v, got feerate=%v, delta=%v", - deadlineA-alicePosition, aliceTxWeight, nextFeeRate, - aliceFeeRate, aliceFeeRateDelta) + // Because Bob is sweeping without deadline pressure, the starting fee + // rate should be the min relay fee rate. + bobStartFeeRate := ht.CalculateTxFeeRate(bobSweepTx) + require.InEpsilonf(ht, uint64(chainfee.FeePerKwFloor), + uint64(bobStartFeeRate), 0.01, "want %v, got %v", + chainfee.FeePerKwFloor, bobStartFeeRate) - require.InEpsilonf(ht, uint64(nextFeeRate), - uint64(aliceFeeRate), 0.02, "want %v, got %v in tx=%v", - nextFeeRate, aliceFeeRate, aliceSweepTx.TxHash()) - } + // With Bob's starting fee rate being validated, we now calculate his + // ending fee rate and fee rate delta. + // + // Bob sweeps one input - the commit output. + bobValue := btcutil.Amount(bobToLocal) + bobBudget := bobValue.MulF64(contractcourt.DefaultBudgetRatio) - reloclateAlicePosition() + // Calculate the ending fee rate and fee rate delta used in his fee + // function. + bobTxWeight := ht.CalculateTxWeight(bobSweepTx) + bobEndingFeeRate := chainfee.NewSatPerKWeight(bobBudget, bobTxWeight) + bobFeeRateDelta := (bobEndingFeeRate - bobStartFeeRate) / + chainfee.SatPerKWeight(deadlineB-1) + expectedFeeRateBob := bobStartFeeRate - // We now mine 7 empty blocks. For each block mined, we'd see Alice's + // We now mine 8 empty blocks. For each block mined, we'd see Alice's // sweeping tx being RBFed. For Bob, he performs a fee bump every - // block, but will only publish a tx every 4 blocks mined as some of + // block, but will only publish a tx every 3 blocks mined as some of // the fee bumps is not sufficient to meet the fee requirements // enforced by RBF. Since his fee function is already at position 1, // mining 7 more blocks means he will RBF his sweeping tx twice. - for i := 1; i < 7; i++ { + for i := 1; i < 9; i++ { // Mine an empty block to trigger the possible RBF attempts. ht.MineEmptyBlocks(1) @@ -1690,9 +1475,9 @@ func testSweepCommitOutputAndAnchor(ht *lntest.HarnessTest) { // Make sure Bob's old sweeping tx has been removed from the // mempool. Since Bob's sweeping tx will only be successfully - // RBFed every 4 blocks, his old sweeping tx only will be - // removed when there are 4 blocks increased. - if bobPosition%4 == 0 { + // RBFed every 3 blocks, his old sweeping tx only will be + // removed when there are 3 blocks increased. + if bobPosition%3 == 0 { ht.AssertTxNotInMempool(bobSweepTx.TxHash()) } @@ -1728,9 +1513,10 @@ func testSweepCommitOutputAndAnchor(ht *lntest.HarnessTest) { aliceFeeRateDelta*chainfee.SatPerKWeight(alicePosition) ht.Logf("Alice(deadline=%v): txWeight=%v, want feerate=%v, "+ - "got feerate=%v, delta=%v", deadlineA-alicePosition, - aliceTxWeight, expectedFeeRateAlice, aliceFeeRate, - aliceFeeRateDelta) + "got feerate=%v, delta=%v in tx %v", + deadlineA-alicePosition, aliceTxWeight, + expectedFeeRateAlice, aliceFeeRate, + aliceFeeRateDelta, aliceSweepTx.TxHash()) require.InEpsilonf(ht, uint64(expectedFeeRateAlice), uint64(aliceFeeRate), 0.02, "want %v, got %v in tx=%v", @@ -1746,16 +1532,17 @@ func testSweepCommitOutputAndAnchor(ht *lntest.HarnessTest) { accumulatedDelta := bobFeeRateDelta * chainfee.SatPerKWeight(bobPosition) - // Bob's sweeping tx will only be successfully RBFed every 4 + // Bob's sweeping tx will only be successfully RBFed every 3 // blocks. - if bobPosition%4 == 0 { + if bobPosition%3 == 0 { expectedFeeRateBob = bobStartFeeRate + accumulatedDelta } ht.Logf("Bob(deadline=%v): txWeight=%v, want feerate=%v, "+ - "got feerate=%v, delta=%v", deadlineB-bobPosition, - bobTxWeight, expectedFeeRateBob, bobFeeRate, - bobFeeRateDelta) + "got feerate=%v, delta=%v in tx %v", + deadlineB-bobPosition, bobTxWeight, + expectedFeeRateBob, bobFeeRate, + bobFeeRateDelta, bobSweepTx.TxHash()) require.InEpsilonf(ht, uint64(expectedFeeRateBob), uint64(bobFeeRate), 0.02, "want %d, got %d in tx=%v", From 0778009ac2ccff3b7f00c68acab55a545099faaa Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 17 Oct 2024 08:41:22 +0800 Subject: [PATCH 69/92] itest: fix `testBumpForceCloseFee` --- itest/lnd_sweep_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/itest/lnd_sweep_test.go b/itest/lnd_sweep_test.go index 4bcca07704c..130b8202d0d 100644 --- a/itest/lnd_sweep_test.go +++ b/itest/lnd_sweep_test.go @@ -1881,6 +1881,7 @@ func testBumpForceCloseFee(ht *lntest.HarnessTest) { if ht.IsNeutrinoBackend() { ht.Skipf("skipping BumpForceCloseFee test for neutrino backend") } + // fundAmt is the funding amount. fundAmt := btcutil.Amount(1_000_000) @@ -1906,6 +1907,7 @@ func testBumpForceCloseFee(ht *lntest.HarnessTest) { // Unwrap the results. chanPoint := chanPoints[0] alice := nodes[0] + bob := nodes[1] // We need to fund alice with 2 wallet inputs so that we can test to // increase the fee rate of the anchor cpfp via two subsequent calls of @@ -2018,6 +2020,10 @@ func testBumpForceCloseFee(ht *lntest.HarnessTest) { txns = ht.GetNumTxsFromMempool(2) ht.FindSweepingTxns(txns, 1, closingTx.TxHash()) + // Shut down Bob, otherwise he will create a sweeping tx to collect the + // to_remote output once Alice's force closing tx is confirmed below. + ht.Shutdown(bob) + // Mine both transactions, the closing tx and the anchor cpfp tx. // This is needed to clean up the mempool. ht.MineBlocksAndAssertNumTxes(1, 2) From e45005b310c70bff65da90e62b20dd31699f1301 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 24 Oct 2024 16:58:02 +0800 Subject: [PATCH 70/92] itest: fix `testPaymentSucceededHTLCRemoteSwept` --- itest/lnd_payment_test.go | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/itest/lnd_payment_test.go b/itest/lnd_payment_test.go index 3515cbc946a..7fb5dfa42d7 100644 --- a/itest/lnd_payment_test.go +++ b/itest/lnd_payment_test.go @@ -135,22 +135,32 @@ func testPaymentSucceededHTLCRemoteSwept(ht *lntest.HarnessTest) { // direct preimage spend. ht.AssertNumPendingSweeps(bob, 1) - // Mine a block to trigger the sweep. - // - // TODO(yy): remove it once `blockbeat` is implemented. - ht.MineEmptyBlocks(1) - - // Mine Bob's sweeping tx. - ht.MineBlocksAndAssertNumTxes(1, 1) - // Let Alice come back up. Since the channel is now closed, we expect // different behaviors based on whether the HTLC is a dust. // - For dust payment, it should be failed now as the HTLC won't go // onchain. // - For non-dust payment, it should be marked as succeeded since her // outgoing htlc is swept by Bob. + // + // TODO(yy): move the restart after Bob's sweeping tx being confirmed + // once the blockbeat starts remembering its last processed block and + // can handle looking for spends in the past blocks. require.NoError(ht, restartAlice()) + // Alice should have a pending force close channel. + ht.AssertNumPendingForceClose(alice, 1) + + // Mine a block to trigger the sweep. This is needed because the + // preimage extraction logic from the link is not managed by the + // blockbeat, which means the preimage may be sent to the contest + // resolver after it's launched. + // + // TODO(yy): Expose blockbeat to the link layer. + ht.MineEmptyBlocks(1) + + // Mine Bob's sweeping tx. + ht.MineBlocksAndAssertNumTxes(1, 1) + // Since Alice is restarted, we need to track the payments again. payStream := alice.RPC.TrackPaymentV2(payHash[:]) dustPayStream := alice.RPC.TrackPaymentV2(dustPayHash[:]) From 9ab9cd5f99d31563a221db3c0eed1aa6971e96fe Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 18 Oct 2024 05:26:52 +0800 Subject: [PATCH 71/92] lntest+itest: start flattening the multi-hop tests Starting from this commit, we begin the process of flattening the multi-hop itests to make them easier to be maintained. The tests are refactored into their own test cases, with each test focusing on testing one channel type. This is necessary to save effort for future development. These tests are also updated to reflect the new `blockbeat` behavior. --- itest/list_on_test.go | 9 +- itest/lnd_multi-hop_force_close_test.go | 361 ++++++++++++++++++++++++ itest/lnd_multi-hop_test.go | 242 ---------------- lntest/harness.go | 154 ++++++++++ lntest/node/config.go | 34 +++ 5 files changed, 554 insertions(+), 246 deletions(-) create mode 100644 itest/lnd_multi-hop_force_close_test.go diff --git a/itest/list_on_test.go b/itest/list_on_test.go index fe6c373855b..6c8baac4dc7 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -153,10 +153,6 @@ var allTestCases = []*lntest.TestCase{ Name: "addpeer config", TestFunc: testAddPeerConfig, }, - { - Name: "multi hop htlc local timeout", - TestFunc: testMultiHopHtlcLocalTimeout, - }, { Name: "multi hop local force close on-chain htlc timeout", TestFunc: testMultiHopLocalForceCloseOnChainHtlcTimeout, @@ -715,3 +711,8 @@ var allTestCases = []*lntest.TestCase{ TestFunc: testQuiescence, }, } + +func init() { + // Register subtests. + allTestCases = append(allTestCases, multiHopForceCloseTestCases...) +} diff --git a/itest/lnd_multi-hop_force_close_test.go b/itest/lnd_multi-hop_force_close_test.go new file mode 100644 index 00000000000..557a61647d2 --- /dev/null +++ b/itest/lnd_multi-hop_force_close_test.go @@ -0,0 +1,361 @@ +package itest + +import ( + "testing" + + "github.com/btcsuite/btcd/btcutil" + "github.com/lightningnetwork/lnd/lncfg" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/routerrpc" + "github.com/lightningnetwork/lnd/lntest" + "github.com/lightningnetwork/lnd/lntest/node" + "github.com/stretchr/testify/require" +) + +const chanAmt = 1000000 + +var leasedType = lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE + +// multiHopForceCloseTestCases defines a set of tests that focuses on the +// behavior of the force close in a multi-hop scenario. +var multiHopForceCloseTestCases = []*lntest.TestCase{ + { + Name: "multihop local claim outgoing htlc anchor", + TestFunc: testLocalClaimOutgoingHTLCAnchor, + }, + { + Name: "multihop local claim outgoing htlc simple taproot", + TestFunc: testLocalClaimOutgoingHTLCSimpleTaproot, + }, + { + Name: "multihop local claim outgoing htlc leased", + TestFunc: testLocalClaimOutgoingHTLCLeased, + }, +} + +// testLocalClaimOutgoingHTLCAnchor tests `runLocalClaimOutgoingHTLC` with +// anchor channel. +func testLocalClaimOutgoingHTLCAnchor(ht *lntest.HarnessTest) { + success := ht.Run("no zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // anchor channels. + // + // Prepare params. + openChannelParams := lntest.OpenChannelParams{Amt: chanAmt} + + cfg := node.CfgAnchor + cfgCarol := append([]string{"--hodl.exit-settle"}, cfg...) + cfgs := [][]string{cfg, cfg, cfgCarol} + + runLocalClaimOutgoingHTLC(st, cfgs, openChannelParams) + }) + if !success { + return + } + + ht.Run("zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // zero-conf anchor channels. + // + // Prepare params. + openChannelParams := lntest.OpenChannelParams{ + Amt: chanAmt, + ZeroConf: true, + CommitmentType: lnrpc.CommitmentType_ANCHORS, + } + + // Prepare Carol's node config to enable zero-conf and anchor. + cfg := node.CfgZeroConf + cfgCarol := append([]string{"--hodl.exit-settle"}, cfg...) + cfgs := [][]string{cfg, cfg, cfgCarol} + + runLocalClaimOutgoingHTLC(st, cfgs, openChannelParams) + }) +} + +// testLocalClaimOutgoingHTLCSimpleTaproot tests `runLocalClaimOutgoingHTLC` +// with simple taproot channel. +func testLocalClaimOutgoingHTLCSimpleTaproot(ht *lntest.HarnessTest) { + c := lnrpc.CommitmentType_SIMPLE_TAPROOT + + success := ht.Run("no zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // simple taproot channels. + // + // Prepare params. + openChannelParams := lntest.OpenChannelParams{ + Amt: chanAmt, + CommitmentType: c, + Private: true, + } + + cfg := node.CfgSimpleTaproot + cfgCarol := append([]string{"--hodl.exit-settle"}, cfg...) + cfgs := [][]string{cfg, cfg, cfgCarol} + + runLocalClaimOutgoingHTLC(st, cfgs, openChannelParams) + }) + if !success { + return + } + + ht.Run("zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // zero-conf simple taproot channels. + // + // Prepare params. + openChannelParams := lntest.OpenChannelParams{ + Amt: chanAmt, + ZeroConf: true, + CommitmentType: c, + Private: true, + } + + // Prepare Carol's node config to enable zero-conf and leased + // channel. + cfg := node.CfgSimpleTaproot + cfg = append(cfg, node.CfgZeroConf...) + cfgCarol := append([]string{"--hodl.exit-settle"}, cfg...) + cfgs := [][]string{cfg, cfg, cfgCarol} + + runLocalClaimOutgoingHTLC(st, cfgs, openChannelParams) + }) +} + +// testLocalClaimOutgoingHTLCLeased tests `runLocalClaimOutgoingHTLC` with +// script enforced lease channel. +func testLocalClaimOutgoingHTLCLeased(ht *lntest.HarnessTest) { + success := ht.Run("no zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // leased channels. + // + // Prepare params. + openChannelParams := lntest.OpenChannelParams{ + Amt: chanAmt, + CommitmentType: leasedType, + } + + cfg := node.CfgLeased + cfgCarol := append([]string{"--hodl.exit-settle"}, cfg...) + cfgs := [][]string{cfg, cfg, cfgCarol} + + runLocalClaimOutgoingHTLC(st, cfgs, openChannelParams) + }) + if !success { + return + } + + ht.Run("zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // zero-conf anchor channels. + // + // Prepare params. + openChannelParams := lntest.OpenChannelParams{ + Amt: chanAmt, + ZeroConf: true, + CommitmentType: leasedType, + } + + // Prepare Carol's node config to enable zero-conf and leased + // channel. + cfg := node.CfgLeased + cfg = append(cfg, node.CfgZeroConf...) + cfgCarol := append([]string{"--hodl.exit-settle"}, cfg...) + cfgs := [][]string{cfg, cfg, cfgCarol} + + runLocalClaimOutgoingHTLC(st, cfgs, openChannelParams) + }) +} + +// runLocalClaimOutgoingHTLC tests that in a multi-hop scenario, if the +// outgoing HTLC is about to time out, then we'll go to chain in order to claim +// it using the HTLC timeout transaction. Any dust HTLC's should be immediately +// canceled backwards. Once the timeout has been reached, then we should sweep +// it on-chain, and cancel the HTLC backwards. +func runLocalClaimOutgoingHTLC(ht *lntest.HarnessTest, + cfgs [][]string, params lntest.OpenChannelParams) { + + // Create a three hop network: Alice -> Bob -> Carol. + _, nodes := ht.CreateSimpleNetwork(cfgs, params) + alice, bob, carol := nodes[0], nodes[1], nodes[2] + + // For neutrino backend, we need to fund one more UTXO for Bob so he + // can sweep his outputs. + if ht.IsNeutrinoBackend() { + ht.FundCoins(btcutil.SatoshiPerBitcoin, bob) + } + + // Now that our channels are set up, we'll send two HTLC's from Alice + // to Carol. The first HTLC will be universally considered "dust", + // while the second will be a proper fully valued HTLC. + const ( + dustHtlcAmt = btcutil.Amount(100) + htlcAmt = btcutil.Amount(300_000) + ) + + // We'll create two random payment hashes unknown to carol, then send + // each of them by manually specifying the HTLC details. + carolPubKey := carol.PubKey[:] + dustPayHash := ht.Random32Bytes() + payHash := ht.Random32Bytes() + + // If this is a taproot channel, then we'll need to make some manual + // route hints so Alice can actually find a route. + var routeHints []*lnrpc.RouteHint + if params.CommitmentType == lnrpc.CommitmentType_SIMPLE_TAPROOT { + routeHints = makeRouteHints(bob, carol, params.ZeroConf) + } + + alice.RPC.SendPayment(&routerrpc.SendPaymentRequest{ + Dest: carolPubKey, + Amt: int64(dustHtlcAmt), + PaymentHash: dustPayHash, + FinalCltvDelta: finalCltvDelta, + TimeoutSeconds: 60, + FeeLimitMsat: noFeeLimitMsat, + RouteHints: routeHints, + }) + + alice.RPC.SendPayment(&routerrpc.SendPaymentRequest{ + Dest: carolPubKey, + Amt: int64(htlcAmt), + PaymentHash: payHash, + FinalCltvDelta: finalCltvDelta, + TimeoutSeconds: 60, + FeeLimitMsat: noFeeLimitMsat, + RouteHints: routeHints, + }) + + // Verify that all nodes in the path now have two HTLC's with the + // proper parameters. + ht.AssertActiveHtlcs(alice, dustPayHash, payHash) + ht.AssertActiveHtlcs(bob, dustPayHash, payHash) + ht.AssertActiveHtlcs(carol, dustPayHash, payHash) + + // We'll now mine enough blocks to trigger Bob's force close the + // channel Bob=>Carol due to the fact that the HTLC is about to + // timeout. With the default outgoing broadcast delta of zero, this + // will be the same height as the htlc expiry height. + numBlocks := padCLTV( + uint32(finalCltvDelta - lncfg.DefaultOutgoingBroadcastDelta), + ) + ht.MineBlocks(int(numBlocks)) + + // Bob's force close tx should have the following outputs, + // 1. anchor output. + // 2. to_local output, which is CSV locked. + // 3. outgoing HTLC output, which has expired. + // + // Bob's anchor output should be offered to his sweeper since Bob has + // time-sensitive HTLCs - we expect both anchors to be offered, while + // the sweeping of the remote anchor will be marked as failed due to + // `testmempoolaccept` check. + // + // For neutrino backend, there's no way to know the sweeping of the + // remote anchor is failed, so Bob still sees two pending sweeps. + if ht.IsNeutrinoBackend() { + ht.AssertNumPendingSweeps(bob, 2) + } else { + ht.AssertNumPendingSweeps(bob, 1) + } + + // We expect to see tow txns in the mempool, + // 1. Bob's force close tx. + // 2. Bob's anchor sweep tx. + ht.AssertNumTxsInMempool(2) + + // Mine a block to confirm the closing tx and the anchor sweeping tx. + ht.MineBlocksAndAssertNumTxes(1, 2) + + // At this point, Bob should have canceled backwards the dust HTLC that + // we sent earlier. This means Alice should now only have a single HTLC + // on her channel. + ht.AssertActiveHtlcs(alice, payHash) + + // With the closing transaction confirmed, we should expect Bob's HTLC + // timeout transaction to be offered to the sweeper due to the expiry + // being reached. we also expect Carol's anchor sweeps. + ht.AssertNumPendingSweeps(bob, 1) + ht.AssertNumPendingSweeps(carol, 1) + + // Bob's sweeper should sweep his outgoing HTLC immediately since it's + // expired. His to_local output cannot be swept due to the CSV lock. + // Carol's anchor sweep should be failed due to output being dust. + ht.AssertNumTxsInMempool(1) + + // Mine a block to confirm Bob's outgoing HTLC sweeping tx. + ht.MineBlocksAndAssertNumTxes(1, 1) + + // With Bob's HTLC timeout transaction confirmed, there should be no + // active HTLC's on the commitment transaction from Alice -> Bob. + ht.AssertNumActiveHtlcs(alice, 0) + + // At this point, Bob should show that the pending HTLC has advanced to + // the second stage and is ready to be swept once the timelock is up. + resp := ht.AssertNumPendingForceClose(bob, 1)[0] + require.NotZero(ht, resp.LimboBalance) + require.Positive(ht, resp.BlocksTilMaturity) + require.Equal(ht, 1, len(resp.PendingHtlcs)) + require.Equal(ht, uint32(2), resp.PendingHtlcs[0].Stage) + + ht.Logf("Bob's timelock to_local output=%v, timelock on second stage "+ + "htlc=%v", resp.BlocksTilMaturity, + resp.PendingHtlcs[0].BlocksTilMaturity) + + if params.CommitmentType == leasedType { + // Since Bob is the initiator of the script-enforced leased + // channel between him and Carol, he will incur an additional + // CLTV on top of the usual CSV delay on any outputs that he + // can sweep back to his wallet. + // + // We now mine enough blocks so the CLTV lock expires, which + // will trigger the sweep of the to_local and outgoing HTLC + // outputs. + ht.MineBlocks(int(resp.BlocksTilMaturity)) + + // Check that Bob has a pending sweeping tx which sweeps his + // to_local and outgoing HTLC outputs. + ht.AssertNumPendingSweeps(bob, 2) + + // Mine a block to confirm the sweeping tx. + ht.MineBlocksAndAssertNumTxes(1, 1) + } else { + // Since Bob force closed the channel between him and Carol, he + // will incur the usual CSV delay on any outputs that he can + // sweep back to his wallet. We'll subtract one block from our + // current maturity period to assert on the mempool. + ht.MineBlocks(int(resp.BlocksTilMaturity - 1)) + + // Check that Bob has a pending sweeping tx which sweeps his + // to_local output. + ht.AssertNumPendingSweeps(bob, 1) + + // Mine a block to confirm the to_local sweeping tx, which also + // triggers the sweeping of the second stage HTLC output. + ht.MineBlocksAndAssertNumTxes(1, 1) + + // Bob's sweeper should now broadcast his second layer sweep + // due to the CSV on the HTLC timeout output. + ht.AssertNumTxsInMempool(1) + + // Next, we'll mine a final block that should confirm the + // sweeping transactions left. + ht.MineBlocksAndAssertNumTxes(1, 1) + } + + // Once this transaction has been confirmed, Bob should detect that he + // no longer has any pending channels. + ht.AssertNumPendingForceClose(bob, 0) +} diff --git a/itest/lnd_multi-hop_test.go b/itest/lnd_multi-hop_test.go index f3a906c141d..f02cd79ee4a 100644 --- a/itest/lnd_multi-hop_test.go +++ b/itest/lnd_multi-hop_test.go @@ -160,248 +160,6 @@ func runMultiHopHtlcClaimTest(ht *lntest.HarnessTest, tester caseRunner) { } } -// testMultiHopHtlcLocalTimeout tests that in a multi-hop HTLC scenario, if the -// outgoing HTLC is about to time out, then we'll go to chain in order to claim -// it using the HTLC timeout transaction. Any dust HTLC's should be immediately -// canceled backwards. Once the timeout has been reached, then we should sweep -// it on-chain, and cancel the HTLC backwards. -func testMultiHopHtlcLocalTimeout(ht *lntest.HarnessTest) { - runMultiHopHtlcClaimTest(ht, runMultiHopHtlcLocalTimeout) -} - -func runMultiHopHtlcLocalTimeout(ht *lntest.HarnessTest, - alice, bob *node.HarnessNode, c lnrpc.CommitmentType, zeroConf bool) { - - // First, we'll create a three hop network: Alice -> Bob -> Carol, with - // Carol refusing to actually settle or directly cancel any HTLC's - // self. - aliceChanPoint, bobChanPoint, carol := createThreeHopNetwork( - ht, alice, bob, true, c, zeroConf, - ) - - // For neutrino backend, we need to fund one more UTXO for Bob so he - // can sweep his outputs. - if ht.IsNeutrinoBackend() { - ht.FundCoins(btcutil.SatoshiPerBitcoin, bob) - } - - // Now that our channels are set up, we'll send two HTLC's from Alice - // to Carol. The first HTLC will be universally considered "dust", - // while the second will be a proper fully valued HTLC. - const ( - dustHtlcAmt = btcutil.Amount(100) - htlcAmt = btcutil.Amount(300_000) - ) - - // We'll create two random payment hashes unknown to carol, then send - // each of them by manually specifying the HTLC details. - carolPubKey := carol.PubKey[:] - dustPayHash := ht.Random32Bytes() - payHash := ht.Random32Bytes() - - // If this is a taproot channel, then we'll need to make some manual - // route hints so Alice can actually find a route. - var routeHints []*lnrpc.RouteHint - if c == lnrpc.CommitmentType_SIMPLE_TAPROOT { - routeHints = makeRouteHints(bob, carol, zeroConf) - } - - alice.RPC.SendPayment(&routerrpc.SendPaymentRequest{ - Dest: carolPubKey, - Amt: int64(dustHtlcAmt), - PaymentHash: dustPayHash, - FinalCltvDelta: finalCltvDelta, - TimeoutSeconds: 60, - FeeLimitMsat: noFeeLimitMsat, - RouteHints: routeHints, - }) - - alice.RPC.SendPayment(&routerrpc.SendPaymentRequest{ - Dest: carolPubKey, - Amt: int64(htlcAmt), - PaymentHash: payHash, - FinalCltvDelta: finalCltvDelta, - TimeoutSeconds: 60, - FeeLimitMsat: noFeeLimitMsat, - RouteHints: routeHints, - }) - - // Verify that all nodes in the path now have two HTLC's with the - // proper parameters. - ht.AssertActiveHtlcs(alice, dustPayHash, payHash) - ht.AssertActiveHtlcs(bob, dustPayHash, payHash) - ht.AssertActiveHtlcs(carol, dustPayHash, payHash) - - // Increase the fee estimate so that the following force close tx will - // be cpfp'ed. - ht.SetFeeEstimate(30000) - - // We'll now mine enough blocks to trigger Bob's broadcast of his - // commitment transaction due to the fact that the HTLC is about to - // timeout. With the default outgoing broadcast delta of zero, this will - // be the same height as the htlc expiry height. - numBlocks := padCLTV( - uint32(finalCltvDelta - lncfg.DefaultOutgoingBroadcastDelta), - ) - ht.MineBlocks(int(numBlocks)) - - // Bob's force close transaction should now be found in the mempool. - ht.AssertNumTxsInMempool(1) - op := ht.OutPointFromChannelPoint(bobChanPoint) - closeTx := ht.AssertOutpointInMempool(op) - - // Dust HTLCs are immediately canceled backwards as soon as the local - // commitment tx is successfully broadcasted to the local mempool. - ht.AssertActiveHtlcs(alice, payHash) - - // Bob's anchor output should be offered to his sweep since Bob has - // time-sensitive HTLCs - we expect both anchors are offered. - ht.AssertNumPendingSweeps(bob, 2) - - // Mine a block to confirm the closing transaction. - ht.MineBlocksAndAssertNumTxes(1, 1) - - // With the closing transaction confirmed, we should expect Bob's HTLC - // timeout transaction to be offered to the sweeper due to the expiry - // being reached. we also expect Bon and Carol's anchor sweeps. - ht.AssertNumPendingSweeps(bob, 2) - ht.AssertNumPendingSweeps(carol, 1) - - // Mine a block to trigger Bob's sweeper to sweep. - ht.MineEmptyBlocks(1) - - // The above mined block would trigger Bob and Carol's sweepers to take - // action. We now expect two txns: - // 1. Bob's sweeping tx anchor sweep should now be found in the mempool. - // 2. Bob's HTLC timeout tx sweep should now be found in the mempool. - // Carol's anchor sweep should be failed due to output being dust. - ht.AssertNumTxsInMempool(2) - - htlcOutpoint := wire.OutPoint{Hash: closeTx.TxHash(), Index: 2} - commitOutpoint := wire.OutPoint{Hash: closeTx.TxHash(), Index: 3} - htlcTimeoutTxid := ht.AssertOutpointInMempool( - htlcOutpoint, - ).TxHash() - - // Mine a block to confirm the above two sweeping txns. - ht.MineBlocksAndAssertNumTxes(1, 2) - - // With Bob's HTLC timeout transaction confirmed, there should be no - // active HTLC's on the commitment transaction from Alice -> Bob. - ht.AssertNumActiveHtlcs(alice, 0) - - // At this point, Bob should show that the pending HTLC has advanced to - // the second stage and is ready to be swept once the timelock is up. - pendingChanResp := bob.RPC.PendingChannels() - require.Equal(ht, 1, len(pendingChanResp.PendingForceClosingChannels)) - forceCloseChan := pendingChanResp.PendingForceClosingChannels[0] - require.NotZero(ht, forceCloseChan.LimboBalance) - require.Positive(ht, forceCloseChan.BlocksTilMaturity) - require.Equal(ht, 1, len(forceCloseChan.PendingHtlcs)) - require.Equal(ht, uint32(2), forceCloseChan.PendingHtlcs[0].Stage) - - ht.Logf("Bob's timelock on commit=%v, timelock on htlc=%v", - forceCloseChan.BlocksTilMaturity, - forceCloseChan.PendingHtlcs[0].BlocksTilMaturity) - - htlcTimeoutOutpoint := wire.OutPoint{Hash: htlcTimeoutTxid, Index: 0} - if c == lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE { - // Since Bob is the initiator of the script-enforced leased - // channel between him and Carol, he will incur an additional - // CLTV on top of the usual CSV delay on any outputs that he - // can sweep back to his wallet. - blocksTilMaturity := int(forceCloseChan.BlocksTilMaturity) - - // We now mine enough blocks to trigger the sweep of the HTLC - // timeout tx. - ht.MineEmptyBlocks(blocksTilMaturity - 1) - - // Check that Bob has one pending sweeping tx - the HTLC - // timeout tx. - ht.AssertNumPendingSweeps(bob, 1) - - // Mine one more blocks, then his commit output will mature. - // This will also trigger the sweeper to sweep his HTLC timeout - // tx. - ht.MineEmptyBlocks(1) - - // Check that Bob has two pending sweeping txns. - ht.AssertNumPendingSweeps(bob, 2) - - // Assert that the HTLC timeout tx is now in the mempool. - ht.AssertOutpointInMempool(htlcTimeoutOutpoint) - - // We now wait for 30 seconds to overcome the flake - there's a - // block race between contractcourt and sweeper, causing the - // sweep to be broadcast earlier. - // - // TODO(yy): remove this once `blockbeat` is in place. - numExpected := 1 - err := wait.NoError(func() error { - mem := ht.GetRawMempool() - if len(mem) == 2 { - numExpected = 2 - return nil - } - - return fmt.Errorf("want %d, got %v in mempool: %v", - numExpected, len(mem), mem) - }, wait.DefaultTimeout) - ht.Logf("Checking mempool got: %v", err) - - // Mine a block to trigger the sweep of his commit output and - // confirm his HTLC timeout sweep. - ht.MineBlocksAndAssertNumTxes(1, numExpected) - - // For leased channels, we need to mine one more block to - // confirm Bob's commit output sweep. - // - // NOTE: we mine this block conditionally, as the commit output - // may have already been swept one block earlier due to the - // race in block consumption among subsystems. - pendingChanResp := bob.RPC.PendingChannels() - if len(pendingChanResp.PendingForceClosingChannels) != 0 { - // Check that the sweep spends the expected inputs. - ht.AssertOutpointInMempool(commitOutpoint) - ht.MineBlocksAndAssertNumTxes(1, 1) - } - } else { - // Since Bob force closed the channel between him and Carol, he - // will incur the usual CSV delay on any outputs that he can - // sweep back to his wallet. We'll subtract one block from our - // current maturity period to assert on the mempool. - numBlocks := int(forceCloseChan.BlocksTilMaturity - 1) - ht.MineEmptyBlocks(numBlocks) - - // Check that Bob has a pending sweeping tx. - ht.AssertNumPendingSweeps(bob, 1) - - // Mine a block the trigger the sweeping behavior. - ht.MineEmptyBlocks(1) - - // Check that the sweep spends from the mined commitment. - ht.AssertOutpointInMempool(commitOutpoint) - - // Mine one more block to trigger the timeout path. - ht.MineBlocksAndAssertNumTxes(1, 1) - - // Bob's sweeper should now broadcast his second layer sweep - // due to the CSV on the HTLC timeout output. - ht.AssertOutpointInMempool(htlcTimeoutOutpoint) - - // Next, we'll mine a final block that should confirm the - // sweeping transactions left. - ht.MineBlocksAndAssertNumTxes(1, 1) - } - - // Once this transaction has been confirmed, Bob should detect that he - // no longer has any pending channels. - ht.AssertNumPendingForceClose(bob, 0) - - // Coop close channel, expect no anchors. - ht.CloseChannel(alice, aliceChanPoint) -} - // testMultiHopReceiverChainClaim tests that in the multi-hop setting, if the // receiver of an HTLC knows the preimage, but wasn't able to settle the HTLC // off-chain, then it goes on chain to claim the HTLC uing the HTLC success diff --git a/lntest/harness.go b/lntest/harness.go index 8e8fcd39362..57602e5dfad 100644 --- a/lntest/harness.go +++ b/lntest/harness.go @@ -8,16 +8,19 @@ import ( "time" "github.com/btcsuite/btcd/blockchain" + "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/go-errors/errors" "github.com/lightningnetwork/lnd/fn/v2" + "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/kvdb/etcd" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" "github.com/lightningnetwork/lnd/lnrpc/routerrpc" + "github.com/lightningnetwork/lnd/lnrpc/signrpc" "github.com/lightningnetwork/lnd/lnrpc/walletrpc" "github.com/lightningnetwork/lnd/lntest/miner" "github.com/lightningnetwork/lnd/lntest/node" @@ -26,6 +29,7 @@ import ( "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing" "github.com/stretchr/testify/require" ) @@ -49,6 +53,9 @@ const ( // maxBlocksAllowed specifies the max allowed value to be used when // mining blocks. maxBlocksAllowed = 100 + + finalCltvDelta = routing.MinCLTVDelta // 18. + thawHeightDelta = finalCltvDelta * 2 // 36. ) // TestCase defines a test case that's been used in the integration test. @@ -2308,12 +2315,40 @@ func (h *HarnessTest) openChannelsForNodes(nodes []*node.HarnessNode, // Sanity check the params. require.Greater(h, len(nodes), 1, "need at least 2 nodes") + // attachFundingShim is a helper closure that optionally attaches a + // funding shim to the open channel params and returns it. + attachFundingShim := func( + nodeA, nodeB *node.HarnessNode) OpenChannelParams { + + // If this channel is not a script enforced lease channel, + // we'll do nothing and return the params. + leasedType := lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE + if p.CommitmentType != leasedType { + return p + } + + // Otherwise derive the funding shim, attach it to the original + // open channel params and return it. + minerHeight := h.CurrentHeight() + thawHeight := minerHeight + thawHeightDelta + fundingShim, _ := h.deriveFundingShim( + nodeA, nodeB, p.Amt, thawHeight, true, leasedType, + ) + + p.FundingShim = fundingShim + + return p + } + // Open channels in batch to save blocks mined. reqs := make([]*OpenChannelRequest, 0, len(nodes)-1) for i := 0; i < len(nodes)-1; i++ { nodeA := nodes[i] nodeB := nodes[i+1] + // Optionally attach a funding shim to the open channel params. + p = attachFundingShim(nodeA, nodeB) + req := &OpenChannelRequest{ Local: nodeA, Remote: nodeB, @@ -2364,3 +2399,122 @@ func (h *HarnessTest) openZeroConfChannelsForNodes(nodes []*node.HarnessNode, return resp } + +// deriveFundingShim creates a channel funding shim by deriving the necessary +// keys on both sides. +func (h *HarnessTest) deriveFundingShim(alice, bob *node.HarnessNode, + chanSize btcutil.Amount, thawHeight uint32, publish bool, + commitType lnrpc.CommitmentType) (*lnrpc.FundingShim, + *lnrpc.ChannelPoint) { + + keyLoc := &signrpc.KeyLocator{KeyFamily: 9999} + carolFundingKey := alice.RPC.DeriveKey(keyLoc) + daveFundingKey := bob.RPC.DeriveKey(keyLoc) + + // Now that we have the multi-sig keys for each party, we can manually + // construct the funding transaction. We'll instruct the backend to + // immediately create and broadcast a transaction paying out an exact + // amount. Normally this would reside in the mempool, but we just + // confirm it now for simplicity. + var ( + fundingOutput *wire.TxOut + musig2 bool + err error + ) + + if commitType == lnrpc.CommitmentType_SIMPLE_TAPROOT || + commitType == lnrpc.CommitmentType_SIMPLE_TAPROOT_OVERLAY { + + var carolKey, daveKey *btcec.PublicKey + carolKey, err = btcec.ParsePubKey(carolFundingKey.RawKeyBytes) + require.NoError(h, err) + daveKey, err = btcec.ParsePubKey(daveFundingKey.RawKeyBytes) + require.NoError(h, err) + + _, fundingOutput, err = input.GenTaprootFundingScript( + carolKey, daveKey, int64(chanSize), + fn.None[chainhash.Hash](), + ) + require.NoError(h, err) + + musig2 = true + } else { + _, fundingOutput, err = input.GenFundingPkScript( + carolFundingKey.RawKeyBytes, daveFundingKey.RawKeyBytes, + int64(chanSize), + ) + require.NoError(h, err) + } + + var txid *chainhash.Hash + targetOutputs := []*wire.TxOut{fundingOutput} + if publish { + txid = h.SendOutputsWithoutChange(targetOutputs, 5) + } else { + tx := h.CreateTransaction(targetOutputs, 5) + + txHash := tx.TxHash() + txid = &txHash + } + + // At this point, we can being our external channel funding workflow. + // We'll start by generating a pending channel ID externally that will + // be used to track this new funding type. + pendingChanID := h.Random32Bytes() + + // Now that we have the pending channel ID, Dave (our responder) will + // register the intent to receive a new channel funding workflow using + // the pending channel ID. + chanPoint := &lnrpc.ChannelPoint{ + FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{ + FundingTxidBytes: txid[:], + }, + } + chanPointShim := &lnrpc.ChanPointShim{ + Amt: int64(chanSize), + ChanPoint: chanPoint, + LocalKey: &lnrpc.KeyDescriptor{ + RawKeyBytes: daveFundingKey.RawKeyBytes, + KeyLoc: &lnrpc.KeyLocator{ + KeyFamily: daveFundingKey.KeyLoc.KeyFamily, + KeyIndex: daveFundingKey.KeyLoc.KeyIndex, + }, + }, + RemoteKey: carolFundingKey.RawKeyBytes, + PendingChanId: pendingChanID, + ThawHeight: thawHeight, + Musig2: musig2, + } + fundingShim := &lnrpc.FundingShim{ + Shim: &lnrpc.FundingShim_ChanPointShim{ + ChanPointShim: chanPointShim, + }, + } + bob.RPC.FundingStateStep(&lnrpc.FundingTransitionMsg{ + Trigger: &lnrpc.FundingTransitionMsg_ShimRegister{ + ShimRegister: fundingShim, + }, + }) + + // If we attempt to register the same shim (has the same pending chan + // ID), then we should get an error. + bob.RPC.FundingStateStepAssertErr(&lnrpc.FundingTransitionMsg{ + Trigger: &lnrpc.FundingTransitionMsg_ShimRegister{ + ShimRegister: fundingShim, + }, + }) + + // We'll take the chan point shim we just registered for Dave (the + // responder), and swap the local/remote keys before we feed it in as + // Carol's funding shim as the initiator. + fundingShim.GetChanPointShim().LocalKey = &lnrpc.KeyDescriptor{ + RawKeyBytes: carolFundingKey.RawKeyBytes, + KeyLoc: &lnrpc.KeyLocator{ + KeyFamily: carolFundingKey.KeyLoc.KeyFamily, + KeyIndex: carolFundingKey.KeyLoc.KeyIndex, + }, + } + fundingShim.GetChanPointShim().RemoteKey = daveFundingKey.RawKeyBytes + + return fundingShim, chanPoint +} diff --git a/lntest/node/config.go b/lntest/node/config.go index 32f24395b6b..6cba60941c3 100644 --- a/lntest/node/config.go +++ b/lntest/node/config.go @@ -41,6 +41,40 @@ var ( btcdExecutable = flag.String( "btcdexec", "", "full path to btcd binary", ) + + // CfgLegacy specifies the config used to create a node that uses the + // legacy channel format. + CfgLegacy = []string{"--protocol.legacy.committweak"} + + // CfgStaticRemoteKey specifies the config used to create a node that + // uses the static remote key feature. + CfgStaticRemoteKey = []string{} + + // CfgAnchor specifies the config used to create a node that uses the + // anchor output feature. + CfgAnchor = []string{"--protocol.anchors"} + + // CfgLeased specifies the config used to create a node that uses the + // leased channel feature. + CfgLeased = []string{ + "--protocol.anchors", + "--protocol.script-enforced-lease", + } + + // CfgSimpleTaproot specifies the config used to create a node that + // uses the simple taproot feature. + CfgSimpleTaproot = []string{ + "--protocol.anchors", + "--protocol.simple-taproot-chans", + } + + // CfgZeroConf specifies the config used to create a node that uses the + // zero-conf channel feature. + CfgZeroConf = []string{ + "--protocol.anchors", + "--protocol.option-scid-alias", + "--protocol.zero-conf", + } ) type DatabaseBackend int From bc31979f7b0551bf288b5193387789a9e7881ff6 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 18 Oct 2024 13:42:23 +0800 Subject: [PATCH 72/92] itest: simplify and flatten `testMultiHopReceiverChainClaim` --- itest/list_on_test.go | 4 - itest/lnd_multi-hop_force_close_test.go | 393 ++++++++++++++++++++++++ itest/lnd_multi-hop_test.go | 252 --------------- 3 files changed, 393 insertions(+), 256 deletions(-) diff --git a/itest/list_on_test.go b/itest/list_on_test.go index 6c8baac4dc7..3aa2d941001 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -13,10 +13,6 @@ var allTestCases = []*lntest.TestCase{ Name: "basic funding flow", TestFunc: testBasicChannelFunding, }, - { - Name: "multi hop receiver chain claim", - TestFunc: testMultiHopReceiverChainClaim, - }, { Name: "external channel funding", TestFunc: testExternalFundingChanPoint, diff --git a/itest/lnd_multi-hop_force_close_test.go b/itest/lnd_multi-hop_force_close_test.go index 557a61647d2..4ba7c0e5c73 100644 --- a/itest/lnd_multi-hop_force_close_test.go +++ b/itest/lnd_multi-hop_force_close_test.go @@ -6,9 +6,11 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/lightningnetwork/lnd/lncfg" "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/lntest" "github.com/lightningnetwork/lnd/lntest/node" + "github.com/lightningnetwork/lnd/lntypes" "github.com/stretchr/testify/require" ) @@ -31,6 +33,18 @@ var multiHopForceCloseTestCases = []*lntest.TestCase{ Name: "multihop local claim outgoing htlc leased", TestFunc: testLocalClaimOutgoingHTLCLeased, }, + { + Name: "multihop receiver preimage claim anchor", + TestFunc: testMultiHopReceiverPreimageClaimAnchor, + }, + { + Name: "multihop receiver preimage claim simple taproot", + TestFunc: testMultiHopReceiverPreimageClaimSimpleTaproot, + }, + { + Name: "multihop receiver preimage claim leased", + TestFunc: testMultiHopReceiverPreimageClaimLeased, + }, } // testLocalClaimOutgoingHTLCAnchor tests `runLocalClaimOutgoingHTLC` with @@ -359,3 +373,382 @@ func runLocalClaimOutgoingHTLC(ht *lntest.HarnessTest, // no longer has any pending channels. ht.AssertNumPendingForceClose(bob, 0) } + +// testMultiHopReceiverPreimageClaimAnchor tests +// `runMultiHopReceiverPreimageClaim` with anchor channels. +func testMultiHopReceiverPreimageClaimAnchor(ht *lntest.HarnessTest) { + success := ht.Run("no zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // anchor channels. + // + // Prepare params. + openChannelParams := lntest.OpenChannelParams{Amt: chanAmt} + + cfg := node.CfgAnchor + cfgs := [][]string{cfg, cfg, cfg} + + runMultiHopReceiverPreimageClaim(st, cfgs, openChannelParams) + }) + if !success { + return + } + + ht.Run("zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // zero-conf anchor channels. + // + // Prepare params. + openChannelParams := lntest.OpenChannelParams{ + Amt: chanAmt, + ZeroConf: true, + CommitmentType: lnrpc.CommitmentType_ANCHORS, + } + + // Prepare Carol's node config to enable zero-conf and anchor. + cfg := node.CfgZeroConf + cfgs := [][]string{cfg, cfg, cfg} + + runMultiHopReceiverPreimageClaim(st, cfgs, openChannelParams) + }) +} + +// testMultiHopReceiverPreimageClaimSimpleTaproot tests +// `runMultiHopReceiverPreimageClaim` with simple taproot channels. +func testMultiHopReceiverPreimageClaimSimpleTaproot(ht *lntest.HarnessTest) { + c := lnrpc.CommitmentType_SIMPLE_TAPROOT + + success := ht.Run("no zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // simple taproot channels. + // + // Prepare params. + openChannelParams := lntest.OpenChannelParams{ + Amt: chanAmt, + CommitmentType: c, + Private: true, + } + + cfg := node.CfgSimpleTaproot + cfgs := [][]string{cfg, cfg, cfg} + + runMultiHopReceiverPreimageClaim(st, cfgs, openChannelParams) + }) + if !success { + return + } + + ht.Run("zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // zero-conf simple taproot channels. + // + // Prepare params. + openChannelParams := lntest.OpenChannelParams{ + Amt: chanAmt, + ZeroConf: true, + CommitmentType: c, + Private: true, + } + + // Prepare Carol's node config to enable zero-conf and leased + // channel. + cfg := node.CfgSimpleTaproot + cfg = append(cfg, node.CfgZeroConf...) + cfgs := [][]string{cfg, cfg, cfg} + + runMultiHopReceiverPreimageClaim(st, cfgs, openChannelParams) + }) +} + +// testMultiHopReceiverPreimageClaimLeased tests +// `runMultiHopReceiverPreimageClaim` with script enforce lease channels. +func testMultiHopReceiverPreimageClaimLeased(ht *lntest.HarnessTest) { + success := ht.Run("no zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // leased channels. + // + // Prepare params. + openChannelParams := lntest.OpenChannelParams{ + Amt: chanAmt, + CommitmentType: leasedType, + } + + cfg := node.CfgLeased + cfgs := [][]string{cfg, cfg, cfg} + + runMultiHopReceiverPreimageClaim(st, cfgs, openChannelParams) + }) + if !success { + return + } + + ht.Run("zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // zero-conf anchor channels. + // + // Prepare params. + openChannelParams := lntest.OpenChannelParams{ + Amt: chanAmt, + ZeroConf: true, + CommitmentType: leasedType, + } + + // Prepare Carol's node config to enable zero-conf and leased + // channel. + cfg := node.CfgLeased + cfg = append(cfg, node.CfgZeroConf...) + cfgs := [][]string{cfg, cfg, cfg} + + runMultiHopReceiverPreimageClaim(st, cfgs, openChannelParams) + }) +} + +// runMultiHopReceiverClaim tests that in the multi-hop setting, if the +// receiver of an HTLC knows the preimage, but wasn't able to settle the HTLC +// off-chain, then it goes on chain to claim the HTLC uing the HTLC success +// transaction. In this scenario, the node that sent the outgoing HTLC should +// extract the preimage from the sweep transaction, and finish settling the +// HTLC backwards into the route. +func runMultiHopReceiverPreimageClaim(ht *lntest.HarnessTest, + cfgs [][]string, params lntest.OpenChannelParams) { + + // Set the min relay feerate to be 10 sat/vbyte so the non-CPFP anchor + // is never swept. + // + // TODO(yy): delete this line once the normal anchor sweeping is + // removed. + ht.SetMinRelayFeerate(10_000) + + // Create a three hop network: Alice -> Bob -> Carol. + chanPoints, nodes := ht.CreateSimpleNetwork(cfgs, params) + alice, bob, carol := nodes[0], nodes[1], nodes[2] + bobChanPoint := chanPoints[1] + + ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) + + // For neutrino backend, we need to one more UTXO for Carol so she can + // sweep her outputs. + if ht.IsNeutrinoBackend() { + ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) + } + + // Fund Carol one UTXO so she can sweep outputs. + ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) + + // If this is a taproot channel, then we'll need to make some manual + // route hints so Alice can actually find a route. + var routeHints []*lnrpc.RouteHint + if params.CommitmentType == lnrpc.CommitmentType_SIMPLE_TAPROOT { + routeHints = makeRouteHints(bob, carol, params.ZeroConf) + } + + // With the network active, we'll now add a new hodl invoice at Carol's + // end. Make sure the cltv expiry delta is large enough, otherwise Bob + // won't send out the outgoing htlc. + const invoiceAmt = 100000 + + var preimage lntypes.Preimage + copy(preimage[:], ht.Random32Bytes()) + payHash := preimage.Hash() + + invoiceReq := &invoicesrpc.AddHoldInvoiceRequest{ + Value: invoiceAmt, + CltvExpiry: finalCltvDelta, + Hash: payHash[:], + RouteHints: routeHints, + } + carolInvoice := carol.RPC.AddHoldInvoice(invoiceReq) + + // Subscribe the invoice. + stream := carol.RPC.SubscribeSingleInvoice(payHash[:]) + + // Now that we've created the invoice, we'll send a single payment from + // Alice to Carol. We won't wait for the response however, as Carol + // will not immediately settle the payment. + req := &routerrpc.SendPaymentRequest{ + PaymentRequest: carolInvoice.PaymentRequest, + TimeoutSeconds: 60, + FeeLimitMsat: noFeeLimitMsat, + } + alice.RPC.SendPayment(req) + + // At this point, all 3 nodes should now have an active channel with + // the created HTLC pending on all of them. + ht.AssertActiveHtlcs(alice, payHash[:]) + ht.AssertActiveHtlcs(bob, payHash[:]) + ht.AssertActiveHtlcs(carol, payHash[:]) + + // Wait for Carol to mark invoice as accepted. There is a small gap to + // bridge between adding the htlc to the channel and executing the exit + // hop logic. + ht.AssertInvoiceState(stream, lnrpc.Invoice_ACCEPTED) + + // Stop Bob so he won't be able to settle the incoming htlc. + restartBob := ht.SuspendNode(bob) + + // Settle invoice. This will just mark the invoice as settled, as there + // is no link anymore to remove the htlc from the commitment tx. For + // this test, it is important to actually settle and not leave the + // invoice in the accepted state, because without a known preimage, the + // channel arbitrator won't go to chain. + carol.RPC.SettleInvoice(preimage[:]) + + // We now advance the block height to the point where Carol will force + // close her channel with Bob, broadcast the closing tx but keep it + // unconfirmed. + numBlocks := padCLTV(uint32( + invoiceReq.CltvExpiry - lncfg.DefaultIncomingBroadcastDelta, + )) + + // Now we'll mine enough blocks to prompt Carol to actually go to the + // chain in order to sweep her HTLC since the value is high enough. + ht.MineBlocks(int(numBlocks)) + + // Carol's force close tx should have the following outputs, + // 1. anchor output. + // 2. to_local output, which is CSV locked. + // 3. incoming HTLC output, which she has the preimage to settle. + // + // Carol's anchor output should be offered to her sweeper since she has + // time-sensitive HTLCs - we expect both anchors to be offered, while + // the sweeping of the remote anchor will be marked as failed due to + // `testmempoolaccept` check. + // + // For neutrino backend, there's no way to know the sweeping of the + // remote anchor is failed, so Carol still sees two pending sweeps. + if ht.IsNeutrinoBackend() { + ht.AssertNumPendingSweeps(carol, 2) + } else { + ht.AssertNumPendingSweeps(carol, 1) + } + + // We expect to see tow txns in the mempool, + // 1. Carol's force close tx. + // 2. Carol's anchor sweep tx. + ht.AssertNumTxsInMempool(2) + + // Mine a block to confirm the closing tx and the anchor sweeping tx. + ht.MineBlocksAndAssertNumTxes(1, 2) + + ht.Log("Current height", ht.CurrentHeight()) + + // After the force close tx is mined, Carol should offer her second + // level HTLC tx to the sweeper. + ht.AssertNumPendingSweeps(carol, 1) + + // Restart bob again. + require.NoError(ht, restartBob()) + + // Once Bob is online, he should notice Carol's second level tx in the + // mempool, he will extract the preimage and settle the HTLC back + // off-chain. He will also try to sweep his anchor and to_local + // outputs, with the anchor output being skipped due to it being + // uneconomical. + if params.CommitmentType == leasedType { + // For leased channels, Bob cannot sweep his to_local output + // yet since it's timelocked, so we only see his anchor input. + ht.AssertNumPendingSweeps(bob, 1) + } else { + // For non-leased channels, Bob should have two pending sweeps, + // 1. to_local output. + // 2. anchor output, tho it won't be swept due to it being + // uneconomical. + ht.AssertNumPendingSweeps(bob, 2) + } + + // Mine an empty block the for neutrino backend. We need this step to + // trigger Bob's chain watcher to detect the force close tx. Deep down, + // this happens because the notification system for neutrino is very + // different from others. Specifically, when a block contains the force + // close tx is notified, these two calls, + // - RegisterBlockEpochNtfn, will notify the block first. + // - RegisterSpendNtfn, will wait for the neutrino notifier to sync to + // the block, then perform a GetUtxo, which, by the time the spend + // details are sent, the blockbeat is considered processed in Bob's + // chain watcher. + // + // TODO(yy): refactor txNotifier to fix the above issue. + if ht.IsNeutrinoBackend() { + ht.MineEmptyBlocks(1) + } + + if params.CommitmentType == leasedType { + // We expect to see 1 txns in the mempool, + // - Carol's second level HTLC sweep tx. + // We now mine a block to confirm it. + ht.MineBlocksAndAssertNumTxes(1, 1) + } else { + // We expect to see 2 txns in the mempool, + // - Bob's to_local sweep tx. + // - Carol's second level HTLC sweep tx. + // We now mine a block to confirm the sweeping txns. + ht.MineBlocksAndAssertNumTxes(1, 2) + } + + // Once the second-level transaction confirmed, Bob should have + // extracted the preimage from the chain, and sent it back to Alice, + // clearing the HTLC off-chain. + ht.AssertNumActiveHtlcs(alice, 0) + + // Check that the Alice's payment is correctly marked succeeded. + ht.AssertPaymentStatus(alice, preimage, lnrpc.Payment_SUCCEEDED) + + // Carol's pending channel report should now show two outputs under + // limbo: her commitment output, as well as the second-layer claim + // output, and the pending HTLC should also now be in stage 2. + ht.AssertNumHTLCsAndStage(carol, bobChanPoint, 1, 2) + + // If we mine 4 additional blocks, then Carol can sweep the second + // level HTLC output once the CSV expires. + ht.MineBlocks(defaultCSV - 1) + + // Assert Carol has the pending HTLC sweep. + ht.AssertNumPendingSweeps(carol, 1) + + // We should have a new transaction in the mempool. + ht.AssertNumTxsInMempool(1) + + // Finally, if we mine an additional block to confirm Carol's second + // level success transaction. Carol should not show a pending channel + // in her report afterwards. + ht.MineBlocksAndAssertNumTxes(1, 1) + ht.AssertNumPendingForceClose(carol, 0) + + // The invoice should show as settled for Carol, indicating that it was + // swept on-chain. + ht.AssertInvoiceSettled(carol, carolInvoice.PaymentAddr) + + // For leased channels, Bob still has his commit output to sweep to + // since he incurred an additional CLTV from being the channel + // initiator. + if params.CommitmentType == leasedType { + resp := ht.AssertNumPendingForceClose(bob, 1)[0] + require.Positive(ht, resp.LimboBalance) + require.Positive(ht, resp.BlocksTilMaturity) + + // Mine enough blocks for Bob's commit output's CLTV to expire + // and sweep it. + ht.MineBlocks(int(resp.BlocksTilMaturity)) + + // Bob should have two pending inputs to be swept, the commit + // output and the anchor output. + ht.AssertNumPendingSweeps(bob, 2) + + // Mine a block to confirm the commit output sweep. + ht.MineBlocksAndAssertNumTxes(1, 1) + } + + // Assert Bob also sees the channel as closed. + ht.AssertNumPendingForceClose(bob, 0) +} diff --git a/itest/lnd_multi-hop_test.go b/itest/lnd_multi-hop_test.go index f02cd79ee4a..e002bbebbc1 100644 --- a/itest/lnd_multi-hop_test.go +++ b/itest/lnd_multi-hop_test.go @@ -160,258 +160,6 @@ func runMultiHopHtlcClaimTest(ht *lntest.HarnessTest, tester caseRunner) { } } -// testMultiHopReceiverChainClaim tests that in the multi-hop setting, if the -// receiver of an HTLC knows the preimage, but wasn't able to settle the HTLC -// off-chain, then it goes on chain to claim the HTLC uing the HTLC success -// transaction. In this scenario, the node that sent the outgoing HTLC should -// extract the preimage from the sweep transaction, and finish settling the -// HTLC backwards into the route. -func testMultiHopReceiverChainClaim(ht *lntest.HarnessTest) { - runMultiHopHtlcClaimTest(ht, runMultiHopReceiverChainClaim) -} - -func runMultiHopReceiverChainClaim(ht *lntest.HarnessTest, - alice, bob *node.HarnessNode, c lnrpc.CommitmentType, zeroConf bool) { - - // First, we'll create a three hop network: Alice -> Bob -> Carol, with - // Carol refusing to actually settle or directly cancel any HTLC's - // self. - aliceChanPoint, bobChanPoint, carol := createThreeHopNetwork( - ht, alice, bob, false, c, zeroConf, - ) - - // For neutrino backend, we need to fund one more UTXO for Carol so she - // can sweep her outputs. - if ht.IsNeutrinoBackend() { - ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) - } - - // If this is a taproot channel, then we'll need to make some manual - // route hints so Alice can actually find a route. - var routeHints []*lnrpc.RouteHint - if c == lnrpc.CommitmentType_SIMPLE_TAPROOT { - routeHints = makeRouteHints(bob, carol, zeroConf) - } - - // With the network active, we'll now add a new hodl invoice at Carol's - // end. Make sure the cltv expiry delta is large enough, otherwise Bob - // won't send out the outgoing htlc. - const invoiceAmt = 100000 - var preimage lntypes.Preimage - copy(preimage[:], ht.Random32Bytes()) - payHash := preimage.Hash() - invoiceReq := &invoicesrpc.AddHoldInvoiceRequest{ - Value: invoiceAmt, - CltvExpiry: finalCltvDelta, - Hash: payHash[:], - RouteHints: routeHints, - } - carolInvoice := carol.RPC.AddHoldInvoice(invoiceReq) - - // Subscribe the invoice. - stream := carol.RPC.SubscribeSingleInvoice(payHash[:]) - - // Now that we've created the invoice, we'll send a single payment from - // Alice to Carol. We won't wait for the response however, as Carol - // will not immediately settle the payment. - req := &routerrpc.SendPaymentRequest{ - PaymentRequest: carolInvoice.PaymentRequest, - TimeoutSeconds: 60, - FeeLimitMsat: noFeeLimitMsat, - } - alice.RPC.SendPayment(req) - - // At this point, all 3 nodes should now have an active channel with - // the created HTLC pending on all of them. - ht.AssertActiveHtlcs(alice, payHash[:]) - ht.AssertActiveHtlcs(bob, payHash[:]) - ht.AssertActiveHtlcs(carol, payHash[:]) - - // Wait for carol to mark invoice as accepted. There is a small gap to - // bridge between adding the htlc to the channel and executing the exit - // hop logic. - ht.AssertInvoiceState(stream, lnrpc.Invoice_ACCEPTED) - - restartBob := ht.SuspendNode(bob) - - // Settle invoice. This will just mark the invoice as settled, as there - // is no link anymore to remove the htlc from the commitment tx. For - // this test, it is important to actually settle and not leave the - // invoice in the accepted state, because without a known preimage, the - // channel arbitrator won't go to chain. - carol.RPC.SettleInvoice(preimage[:]) - - // Increase the fee estimate so that the following force close tx will - // be cpfp'ed. - ht.SetFeeEstimate(30000) - - // We now advance the block height to the point where Carol will force - // close her channel with Bob, broadcast the closing tx but keep it - // unconfirmed. - numBlocks := padCLTV(uint32( - invoiceReq.CltvExpiry - lncfg.DefaultIncomingBroadcastDelta, - )) - - // Now we'll mine enough blocks to prompt carol to actually go to the - // chain in order to sweep her HTLC since the value is high enough. - ht.MineEmptyBlocks(int(numBlocks)) - - // At this point, Carol should broadcast her active commitment - // transaction in order to go to the chain and sweep her HTLC. - ht.AssertNumTxsInMempool(1) - - closingTx := ht.AssertOutpointInMempool( - ht.OutPointFromChannelPoint(bobChanPoint), - ) - closingTxid := closingTx.TxHash() - - // Carol's anchor should have been offered to her sweeper as she has - // time-sensitive HTLCs. Assert that we have two anchors - one for the - // anchor on the local commitment and the other for the anchor on the - // remote commitment (invalid). - ht.AssertNumPendingSweeps(carol, 2) - - // Confirm the commitment. - ht.MineBlocksAndAssertNumTxes(1, 1) - - // The above mined block will trigger Carol's sweeper to publish the - // anchor sweeping tx. - // - // TODO(yy): should instead cancel the broadcast of the anchor sweeping - // tx to save fees since we know the force close tx has been confirmed? - // This is very difficult as it introduces more complicated RBF - // scenarios, as we are using a wallet utxo, which means any txns using - // that wallet utxo must pay more fees. On the other hand, there's no - // way to remove that anchor-CPFP tx from the mempool. - ht.AssertNumTxsInMempool(1) - - // After the force close transaction is mined, Carol should offer her - // second level HTLC tx to the sweeper, which means we should see two - // pending inputs now - the anchor and the htlc. - ht.AssertNumPendingSweeps(carol, 2) - - // Restart bob again. - require.NoError(ht, restartBob()) - - var expectedTxes int - - // After the force close transaction is mined, a series of transactions - // should be broadcast by Bob and Carol. When Bob notices Carol's - // second level transaction in the mempool, he will extract the - // preimage and settle the HTLC back off-chain. - switch c { - // We expect to see three txns in the mempool: - // 1. Carol should broadcast her second level HTLC tx. - // 2. Carol should broadcast her anchor sweeping tx. - // 3. Bob should broadcast a sweep tx to sweep his output in the - // channel with Carol, and in the same sweep tx to sweep his anchor - // output. - case lnrpc.CommitmentType_ANCHORS, lnrpc.CommitmentType_SIMPLE_TAPROOT: - expectedTxes = 3 - ht.AssertNumPendingSweeps(bob, 2) - - // We expect to see two txns in the mempool: - // 1. Carol should broadcast her second level HTLC tx. - // 2. Carol should broadcast her anchor sweeping tx. - // Bob would offer his anchor output to his sweeper, but it cannot be - // swept due to it being uneconomical. Bob's commit output can't be - // swept yet as he's incurring an additional CLTV from being the - // channel initiator of a script-enforced leased channel. - case lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE: - expectedTxes = 2 - ht.AssertNumPendingSweeps(bob, 1) - - default: - ht.Fatalf("unhandled commitment type %v", c) - } - - // Mine one block to trigger the sweeper to sweep. - ht.MineEmptyBlocks(1) - - // All transactions should be spending from the commitment transaction. - txes := ht.GetNumTxsFromMempool(expectedTxes) - ht.AssertAllTxesSpendFrom(txes, closingTxid) - - // We'll now mine an additional block which should confirm both the - // second layer transactions. - ht.MineBlocksAndAssertNumTxes(1, expectedTxes) - - // Carol's pending channel report should now show two outputs under - // limbo: her commitment output, as well as the second-layer claim - // output, and the pending HTLC should also now be in stage 2. - ht.AssertNumHTLCsAndStage(carol, bobChanPoint, 1, 2) - - // Once the second-level transaction confirmed, Bob should have - // extracted the preimage from the chain, and sent it back to Alice, - // clearing the HTLC off-chain. - ht.AssertNumActiveHtlcs(alice, 0) - - // If we mine 4 additional blocks, then Carol can sweep the second - // level HTLC output once the CSV expires. - ht.MineEmptyBlocks(defaultCSV - 1) - - // Assert Carol has the pending HTLC sweep. - ht.AssertNumPendingSweeps(carol, 1) - - // Mine one block to trigger the sweeper to sweep. - ht.MineEmptyBlocks(1) - - // We should have a new transaction in the mempool. - ht.AssertNumTxsInMempool(1) - - // Finally, if we mine an additional block to confirm Carol's second - // level success transaction. Carol should not show a pending channel - // in her report afterwards. - ht.MineBlocksAndAssertNumTxes(1, 1) - ht.AssertNumPendingForceClose(carol, 0) - - // The invoice should show as settled for Carol, indicating that it was - // swept on-chain. - ht.AssertInvoiceSettled(carol, carolInvoice.PaymentAddr) - - // Finally, check that the Alice's payment is correctly marked - // succeeded. - ht.AssertPaymentStatus(alice, preimage, lnrpc.Payment_SUCCEEDED) - - if c == lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE { - // Bob still has his commit output to sweep to since he - // incurred an additional CLTV from being the channel initiator - // of a script-enforced leased channel, regardless of whether - // he forced closed the channel or not. - pendingChanResp := bob.RPC.PendingChannels() - - require.Len(ht, pendingChanResp.PendingForceClosingChannels, 1) - forceCloseChan := pendingChanResp.PendingForceClosingChannels[0] - require.Positive(ht, forceCloseChan.LimboBalance) - require.Positive(ht, forceCloseChan.BlocksTilMaturity) - - // TODO: Bob still shows a pending HTLC at this point when he - // shouldn't, as he already extracted the preimage from Carol's - // claim. - // require.Len(t.t, forceCloseChan.PendingHtlcs, 0) - - // Mine enough blocks for Bob's commit output's CLTV to expire - // and sweep it. - numBlocks := int(forceCloseChan.BlocksTilMaturity) - ht.MineEmptyBlocks(numBlocks) - - // Bob should have two pending inputs to be swept, the commit - // output and the anchor output. - ht.AssertNumPendingSweeps(bob, 2) - ht.MineEmptyBlocks(1) - - commitOutpoint := wire.OutPoint{Hash: closingTxid, Index: 3} - ht.AssertOutpointInMempool(commitOutpoint) - ht.MineBlocksAndAssertNumTxes(1, 1) - } - - ht.AssertNumPendingForceClose(bob, 0) - - // We'll close out the channel between Alice and Bob, then shutdown - // carol to conclude the test. - ht.CloseChannel(alice, aliceChanPoint) -} - // testMultiHopLocalForceCloseOnChainHtlcTimeout tests that in a multi-hop HTLC // scenario, if the node that extended the HTLC to the final node closes their // commitment on-chain early, then it eventually recognizes this HTLC as one From bef17f16cf53c67b627dce430eaf50f7572432b7 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 19 Oct 2024 07:35:40 +0800 Subject: [PATCH 73/92] lntest+itest: flatten `testMultiHopLocalForceCloseOnChainHtlcTimeout` --- itest/list_on_test.go | 4 - itest/lnd_multi-hop_force_close_test.go | 347 +++++++++++++++++++++++- itest/lnd_multi-hop_test.go | 195 ------------- lntest/harness_assertion.go | 8 +- 4 files changed, 348 insertions(+), 206 deletions(-) diff --git a/itest/list_on_test.go b/itest/list_on_test.go index 3aa2d941001..d11a6e5bbc7 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -149,10 +149,6 @@ var allTestCases = []*lntest.TestCase{ Name: "addpeer config", TestFunc: testAddPeerConfig, }, - { - Name: "multi hop local force close on-chain htlc timeout", - TestFunc: testMultiHopLocalForceCloseOnChainHtlcTimeout, - }, { Name: "multi hop remote force close on-chain htlc timeout", TestFunc: testMultiHopRemoteForceCloseOnChainHtlcTimeout, diff --git a/itest/lnd_multi-hop_force_close_test.go b/itest/lnd_multi-hop_force_close_test.go index 4ba7c0e5c73..49581a8b9c7 100644 --- a/itest/lnd_multi-hop_force_close_test.go +++ b/itest/lnd_multi-hop_force_close_test.go @@ -14,12 +14,17 @@ import ( "github.com/stretchr/testify/require" ) -const chanAmt = 1000000 +const ( + chanAmt = 1000000 + htlcAmt = btcutil.Amount(300_000) +) var leasedType = lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE // multiHopForceCloseTestCases defines a set of tests that focuses on the // behavior of the force close in a multi-hop scenario. +// +//nolint:ll var multiHopForceCloseTestCases = []*lntest.TestCase{ { Name: "multihop local claim outgoing htlc anchor", @@ -45,6 +50,18 @@ var multiHopForceCloseTestCases = []*lntest.TestCase{ Name: "multihop receiver preimage claim leased", TestFunc: testMultiHopReceiverPreimageClaimLeased, }, + { + Name: "multihop local force close before timeout anchor", + TestFunc: testLocalForceCloseBeforeTimeoutAnchor, + }, + { + Name: "multihop local force close before timeout simple taproot", + TestFunc: testLocalForceCloseBeforeTimeoutSimpleTaproot, + }, + { + Name: "multihop local force close before timeout leased", + TestFunc: testLocalForceCloseBeforeTimeoutLeased, + }, } // testLocalClaimOutgoingHTLCAnchor tests `runLocalClaimOutgoingHTLC` with @@ -214,10 +231,7 @@ func runLocalClaimOutgoingHTLC(ht *lntest.HarnessTest, // Now that our channels are set up, we'll send two HTLC's from Alice // to Carol. The first HTLC will be universally considered "dust", // while the second will be a proper fully valued HTLC. - const ( - dustHtlcAmt = btcutil.Amount(100) - htlcAmt = btcutil.Amount(300_000) - ) + const dustHtlcAmt = btcutil.Amount(100) // We'll create two random payment hashes unknown to carol, then send // each of them by manually specifying the HTLC details. @@ -752,3 +766,326 @@ func runMultiHopReceiverPreimageClaim(ht *lntest.HarnessTest, // Assert Bob also sees the channel as closed. ht.AssertNumPendingForceClose(bob, 0) } + +// testLocalForceCloseBeforeTimeoutAnchor tests +// `runLocalForceCloseBeforeHtlcTimeout` with anchor channel. +func testLocalForceCloseBeforeTimeoutAnchor(ht *lntest.HarnessTest) { + success := ht.Run("no zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // anchor channels. + // + // Prepare params. + params := lntest.OpenChannelParams{Amt: chanAmt} + + cfg := node.CfgAnchor + cfgCarol := append([]string{"--hodl.exit-settle"}, cfg...) + cfgs := [][]string{cfg, cfg, cfgCarol} + + runLocalForceCloseBeforeHtlcTimeout(st, cfgs, params) + }) + if !success { + return + } + + ht.Run("zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // zero-conf anchor channels. + // + // Prepare params. + params := lntest.OpenChannelParams{ + Amt: chanAmt, + ZeroConf: true, + CommitmentType: lnrpc.CommitmentType_ANCHORS, + } + + // Prepare Carol's node config to enable zero-conf and anchor. + cfg := node.CfgZeroConf + cfgCarol := append([]string{"--hodl.exit-settle"}, cfg...) + cfgs := [][]string{cfg, cfg, cfgCarol} + + runLocalForceCloseBeforeHtlcTimeout(st, cfgs, params) + }) +} + +// testLocalForceCloseBeforeTimeoutSimpleTaproot tests +// `runLocalForceCloseBeforeHtlcTimeout` with simple taproot channel. +func testLocalForceCloseBeforeTimeoutSimpleTaproot(ht *lntest.HarnessTest) { + c := lnrpc.CommitmentType_SIMPLE_TAPROOT + + success := ht.Run("no zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // simple taproot channels. + // + // Prepare params. + params := lntest.OpenChannelParams{ + Amt: chanAmt, + CommitmentType: c, + Private: true, + } + + cfg := node.CfgSimpleTaproot + cfgCarol := append([]string{"--hodl.exit-settle"}, cfg...) + cfgs := [][]string{cfg, cfg, cfgCarol} + + runLocalForceCloseBeforeHtlcTimeout(st, cfgs, params) + }) + if !success { + return + } + + ht.Run("zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // zero-conf simple taproot channels. + // + // Prepare params. + params := lntest.OpenChannelParams{ + Amt: chanAmt, + ZeroConf: true, + CommitmentType: c, + Private: true, + } + + // Prepare Carol's node config to enable zero-conf and leased + // channel. + cfg := node.CfgSimpleTaproot + cfg = append(cfg, node.CfgZeroConf...) + cfgCarol := append([]string{"--hodl.exit-settle"}, cfg...) + cfgs := [][]string{cfg, cfg, cfgCarol} + + runLocalForceCloseBeforeHtlcTimeout(st, cfgs, params) + }) +} + +// testLocalForceCloseBeforeTimeoutLeased tests +// `runLocalForceCloseBeforeHtlcTimeout` with script enforced lease channel. +func testLocalForceCloseBeforeTimeoutLeased(ht *lntest.HarnessTest) { + success := ht.Run("no zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // leased channels. + // + // Prepare params. + params := lntest.OpenChannelParams{ + Amt: chanAmt, + CommitmentType: leasedType, + } + + cfg := node.CfgLeased + cfgCarol := append([]string{"--hodl.exit-settle"}, cfg...) + cfgs := [][]string{cfg, cfg, cfgCarol} + + runLocalForceCloseBeforeHtlcTimeout(st, cfgs, params) + }) + if !success { + return + } + + ht.Run("zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // zero-conf anchor channels. + // + // Prepare params. + params := lntest.OpenChannelParams{ + Amt: chanAmt, + ZeroConf: true, + CommitmentType: leasedType, + } + + // Prepare Carol's node config to enable zero-conf and leased + // channel. + cfg := node.CfgLeased + cfg = append(cfg, node.CfgZeroConf...) + cfgCarol := append([]string{"--hodl.exit-settle"}, cfg...) + cfgs := [][]string{cfg, cfg, cfgCarol} + + runLocalForceCloseBeforeHtlcTimeout(st, cfgs, params) + }) +} + +// runLocalForceCloseBeforeHtlcTimeout tests that in a multi-hop HTLC scenario, +// if the node that extended the HTLC to the final node closes their commitment +// on-chain early, then it eventually recognizes this HTLC as one that's timed +// out. At this point, the node should timeout the HTLC using the HTLC timeout +// transaction, then cancel it backwards as normal. +func runLocalForceCloseBeforeHtlcTimeout(ht *lntest.HarnessTest, + cfgs [][]string, params lntest.OpenChannelParams) { + + // Set the min relay feerate to be 10 sat/vbyte so the non-CPFP anchor + // is never swept. + // + // TODO(yy): delete this line once the normal anchor sweeping is + // removed. + ht.SetMinRelayFeerate(10_000) + + // Create a three hop network: Alice -> Bob -> Carol. + chanPoints, nodes := ht.CreateSimpleNetwork(cfgs, params) + alice, bob, carol := nodes[0], nodes[1], nodes[2] + bobChanPoint := chanPoints[1] + + // With our channels set up, we'll then send a single HTLC from Alice + // to Carol. As Carol is in hodl mode, she won't settle this HTLC which + // opens up the base for out tests. + + // If this is a taproot channel, then we'll need to make some manual + // route hints so Alice can actually find a route. + var routeHints []*lnrpc.RouteHint + if params.CommitmentType == lnrpc.CommitmentType_SIMPLE_TAPROOT { + routeHints = makeRouteHints(bob, carol, params.ZeroConf) + } + + // We'll now send a single HTLC across our multi-hop network. + carolPubKey := carol.PubKey[:] + payHash := ht.Random32Bytes() + req := &routerrpc.SendPaymentRequest{ + Dest: carolPubKey, + Amt: int64(htlcAmt), + PaymentHash: payHash, + FinalCltvDelta: finalCltvDelta, + TimeoutSeconds: 60, + FeeLimitMsat: noFeeLimitMsat, + RouteHints: routeHints, + } + alice.RPC.SendPayment(req) + + // Once the HTLC has cleared, all channels in our mini network should + // have the it locked in. + ht.AssertActiveHtlcs(alice, payHash) + ht.AssertActiveHtlcs(bob, payHash) + ht.AssertActiveHtlcs(carol, payHash) + + // Now that all parties have the HTLC locked in, we'll immediately + // force close the Bob -> Carol channel. This should trigger contract + // resolution mode for both of them. + stream, _ := ht.CloseChannelAssertPending(bob, bobChanPoint, true) + ht.AssertStreamChannelForceClosed(bob, bobChanPoint, true, stream) + + // Bob's force close tx should have the following outputs, + // 1. anchor output. + // 2. to_local output, which is CSV locked. + // 3. outgoing HTLC output, which hasn't expired yet. + // + // The channel close has anchors, we should expect to see both Bob and + // Carol has a pending sweep request for the anchor sweep. + ht.AssertNumPendingSweeps(carol, 1) + anchorSweep := ht.AssertNumPendingSweeps(bob, 1)[0] + + // We expcet Bob's anchor sweep to be a non-CPFP anchor sweep now. + // Although he has time-sensitive outputs, which means initially his + // anchor output was used for CPFP, this anchor will be replaced by a + // new anchor sweeping request once his force close tx is confirmed in + // the above block. The timeline goes as follows: + // 1. At block 447, Bob force closes his channel with Carol, which + // caused the channel arbitartor to create a CPFP anchor sweep. + // 2. This force close tx was mined in AssertStreamChannelForceClosed, + // and we are now in block 448. + // 3. Since the blockbeat is processed via the chain [ChainArbitrator + // -> chainWatcher -> channelArbitrator -> Sweeper -> TxPublisher], + // when it reaches `chainWatcher`, Bob will detect the confirmed + // force close tx and notifies `channelArbitrator`. In response, + // `channelArbitrator` will advance to `StateContractClosed`, in + // which it will prepare an anchor resolution that's non-CPFP, send + // it to the sweeper to replace the CPFP anchor sweep. + // 4. By the time block 448 reaches `Sweeper`, the old CPFP anchor + // sweep has already been replaced with the new non-CPFP anchor + // sweep. + require.EqualValues(ht, 330, anchorSweep.Budget, "expected 330 sat "+ + "budget, got %v", anchorSweep.Budget) + + // Before the HTLC times out, we'll need to assert that Bob broadcasts + // a sweep tx for his commit output. Note that if the channel has a + // script-enforced lease, then Bob will have to wait for an additional + // CLTV before sweeping it. + if params.CommitmentType != leasedType { + // The sweeping tx is broadcast on the block CSV-1 so mine one + // block less than defaultCSV in order to perform mempool + // assertions. + ht.MineBlocks(int(defaultCSV - 1)) + + // Mine a block to confirm Bob's to_local sweep. + ht.MineBlocksAndAssertNumTxes(1, 1) + } + + // We'll now mine enough blocks for the HTLC to expire. After this, Bob + // should hand off the now expired HTLC output to the sweeper. + resp := ht.AssertNumPendingForceClose(bob, 1)[0] + require.Equal(ht, 1, len(resp.PendingHtlcs)) + + ht.Logf("Bob's timelock to_local output=%v, timelock on second stage "+ + "htlc=%v", resp.BlocksTilMaturity, + resp.PendingHtlcs[0].BlocksTilMaturity) + + ht.MineBlocks(int(resp.PendingHtlcs[0].BlocksTilMaturity)) + + // Bob's pending channel report should show that he has a single HTLC + // that's now in stage one. + ht.AssertNumHTLCsAndStage(bob, bobChanPoint, 1, 1) + + // Bob should have two pending sweep requests, + // 1. the anchor sweep. + // 2. the outgoing HTLC sweep. + ht.AssertNumPendingSweeps(bob, 2) + + // Bob's outgoing HTLC sweep should be broadcast now. Mine a block to + // confirm it. + ht.MineBlocksAndAssertNumTxes(1, 1) + + // With the second layer timeout tx confirmed, Bob should have canceled + // backwards the HTLC that Carol sent. + ht.AssertNumActiveHtlcs(bob, 0) + + // Additionally, Bob should now show that HTLC as being advanced to the + // second stage. + ht.AssertNumHTLCsAndStage(bob, bobChanPoint, 1, 2) + + // Get the expiry height of the CSV-locked HTLC. + resp = ht.AssertNumPendingForceClose(bob, 1)[0] + require.Equal(ht, 1, len(resp.PendingHtlcs)) + pendingHtlc := resp.PendingHtlcs[0] + require.Positive(ht, pendingHtlc.BlocksTilMaturity) + + ht.Logf("Bob's timelock to_local output=%v, timelock on second stage "+ + "htlc=%v", resp.BlocksTilMaturity, + resp.PendingHtlcs[0].BlocksTilMaturity) + + // Mine enough blocks for the HTLC to expire. + ht.MineBlocks(int(pendingHtlc.BlocksTilMaturity)) + + // Based on this is a leased channel or not, Bob may still need to + // sweep his to_local output. + if params.CommitmentType == leasedType { + // Bob should have three pending sweep requests, + // 1. the anchor sweep. + // 2. the second-level HTLC sweep. + // 3. the to_local output sweep, which is CSV+CLTV locked, is + // now mature. + // + // The test is setup such that the to_local and the + // second-level HTLC sweeps share the same deadline, which + // means they will be swept in the same tx. + ht.AssertNumPendingSweeps(bob, 3) + } else { + // Bob should have two pending sweeps, + // 1. the anchor sweep. + // 2. the second-level HTLC sweep. + ht.AssertNumPendingSweeps(bob, 2) + } + + // Now that the CSV timelock has expired, mine a block to confirm the + // sweep. + ht.MineBlocksAndAssertNumTxes(1, 1) + + // At this point, Bob should no longer show any channels as pending + // close. + ht.AssertNumPendingForceClose(bob, 0) +} diff --git a/itest/lnd_multi-hop_test.go b/itest/lnd_multi-hop_test.go index e002bbebbc1..2a866f7929b 100644 --- a/itest/lnd_multi-hop_test.go +++ b/itest/lnd_multi-hop_test.go @@ -160,201 +160,6 @@ func runMultiHopHtlcClaimTest(ht *lntest.HarnessTest, tester caseRunner) { } } -// testMultiHopLocalForceCloseOnChainHtlcTimeout tests that in a multi-hop HTLC -// scenario, if the node that extended the HTLC to the final node closes their -// commitment on-chain early, then it eventually recognizes this HTLC as one -// that's timed out. At this point, the node should timeout the HTLC using the -// HTLC timeout transaction, then cancel it backwards as normal. -func testMultiHopLocalForceCloseOnChainHtlcTimeout(ht *lntest.HarnessTest) { - runMultiHopHtlcClaimTest( - ht, runMultiHopLocalForceCloseOnChainHtlcTimeout, - ) -} - -func runMultiHopLocalForceCloseOnChainHtlcTimeout(ht *lntest.HarnessTest, - alice, bob *node.HarnessNode, c lnrpc.CommitmentType, zeroConf bool) { - - // First, we'll create a three hop network: Alice -> Bob -> Carol, with - // Carol refusing to actually settle or directly cancel any HTLC's - // self. - aliceChanPoint, bobChanPoint, carol := createThreeHopNetwork( - ht, alice, bob, true, c, zeroConf, - ) - - // With our channels set up, we'll then send a single HTLC from Alice - // to Carol. As Carol is in hodl mode, she won't settle this HTLC which - // opens up the base for out tests. - const htlcAmt = btcutil.Amount(300_000) - - // If this is a taproot channel, then we'll need to make some manual - // route hints so Alice can actually find a route. - var routeHints []*lnrpc.RouteHint - if c == lnrpc.CommitmentType_SIMPLE_TAPROOT { - routeHints = makeRouteHints(bob, carol, zeroConf) - } - - // We'll now send a single HTLC across our multi-hop network. - carolPubKey := carol.PubKey[:] - payHash := ht.Random32Bytes() - req := &routerrpc.SendPaymentRequest{ - Dest: carolPubKey, - Amt: int64(htlcAmt), - PaymentHash: payHash, - FinalCltvDelta: finalCltvDelta, - TimeoutSeconds: 60, - FeeLimitMsat: noFeeLimitMsat, - RouteHints: routeHints, - } - alice.RPC.SendPayment(req) - - // Once the HTLC has cleared, all channels in our mini network should - // have the it locked in. - ht.AssertActiveHtlcs(alice, payHash) - ht.AssertActiveHtlcs(bob, payHash) - ht.AssertActiveHtlcs(carol, payHash) - - // blocksMined records how many blocks have mined after the creation of - // the invoice so it can be used to calculate how many more blocks need - // to be mined to trigger a force close later on. - var blocksMined uint32 - - // Now that all parties have the HTLC locked in, we'll immediately - // force close the Bob -> Carol channel. This should trigger contract - // resolution mode for both of them. - stream, _ := ht.CloseChannelAssertPending(bob, bobChanPoint, true) - closeTx := ht.AssertStreamChannelForceClosed( - bob, bobChanPoint, true, stream, - ) - - // Increase the blocks mined. At the step - // AssertStreamChannelForceClosed mines one block. - blocksMined++ - - // The channel close has anchors, we should expect to see both Bob and - // Carol has a pending sweep request for the anchor sweep. - ht.AssertNumPendingSweeps(carol, 1) - ht.AssertNumPendingSweeps(bob, 1) - - // Mine a block to confirm Bob's anchor sweep - Carol's anchor sweep - // won't succeed because it's not used for CPFP, so there's no wallet - // utxo used, resulting it to be uneconomical. - ht.MineBlocksAndAssertNumTxes(1, 1) - blocksMined++ - - htlcOutpoint := wire.OutPoint{Hash: closeTx, Index: 2} - bobCommitOutpoint := wire.OutPoint{Hash: closeTx, Index: 3} - - // Before the HTLC times out, we'll need to assert that Bob broadcasts - // a sweep transaction for his commit output. Note that if the channel - // has a script-enforced lease, then Bob will have to wait for an - // additional CLTV before sweeping it. - if c != lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE { - // The sweep is broadcast on the block immediately before the - // CSV expires and the commitment was already mined inside - // AssertStreamChannelForceClosed(), so mine one block less - // than defaultCSV in order to perform mempool assertions. - ht.MineEmptyBlocks(int(defaultCSV - blocksMined)) - blocksMined = defaultCSV - - // Assert Bob has the sweep and trigger it. - ht.AssertNumPendingSweeps(bob, 1) - ht.MineEmptyBlocks(1) - blocksMined++ - - commitSweepTx := ht.AssertOutpointInMempool( - bobCommitOutpoint, - ) - txid := commitSweepTx.TxHash() - block := ht.MineBlocksAndAssertNumTxes(1, 1)[0] - ht.AssertTxInBlock(block, txid) - - blocksMined++ - } - - // We'll now mine enough blocks for the HTLC to expire. After this, Bob - // should hand off the now expired HTLC output to the utxo nursery. - numBlocks := padCLTV(uint32(finalCltvDelta) - - lncfg.DefaultOutgoingBroadcastDelta) - ht.MineEmptyBlocks(int(numBlocks - blocksMined)) - - // Bob's pending channel report should show that he has a single HTLC - // that's now in stage one. - ht.AssertNumHTLCsAndStage(bob, bobChanPoint, 1, 1) - - // Bob should have a pending sweep request. - ht.AssertNumPendingSweeps(bob, 1) - - // Mine one block to trigger Bob's sweeper to sweep it. - ht.MineEmptyBlocks(1) - - // We should also now find a transaction in the mempool, as Bob should - // have broadcast his second layer timeout transaction. - timeoutTx := ht.AssertOutpointInMempool(htlcOutpoint).TxHash() - - // Next, we'll mine an additional block. This should serve to confirm - // the second layer timeout transaction. - block := ht.MineBlocksAndAssertNumTxes(1, 1)[0] - ht.AssertTxInBlock(block, timeoutTx) - - // With the second layer timeout transaction confirmed, Bob should have - // canceled backwards the HTLC that carol sent. - ht.AssertNumActiveHtlcs(bob, 0) - - // Additionally, Bob should now show that HTLC as being advanced to the - // second stage. - ht.AssertNumHTLCsAndStage(bob, bobChanPoint, 1, 2) - - // Bob should now broadcast a transaction that sweeps certain inputs - // depending on the commitment type. We'll need to mine some blocks - // before the broadcast is possible. - resp := bob.RPC.PendingChannels() - - require.Len(ht, resp.PendingForceClosingChannels, 1) - forceCloseChan := resp.PendingForceClosingChannels[0] - require.Len(ht, forceCloseChan.PendingHtlcs, 1) - pendingHtlc := forceCloseChan.PendingHtlcs[0] - require.Positive(ht, pendingHtlc.BlocksTilMaturity) - numBlocks = uint32(pendingHtlc.BlocksTilMaturity) - - ht.MineEmptyBlocks(int(numBlocks)) - - var numExpected int - - // Now that the CSV/CLTV timelock has expired, the transaction should - // either only sweep the HTLC timeout transaction, or sweep both the - // HTLC timeout transaction and Bob's commit output depending on the - // commitment type. - if c == lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE { - // Assert the expected number of pending sweeps are found. - sweeps := ht.AssertNumPendingSweeps(bob, 2) - - numExpected = 1 - if sweeps[0].DeadlineHeight != sweeps[1].DeadlineHeight { - numExpected = 2 - } - } else { - ht.AssertNumPendingSweeps(bob, 1) - numExpected = 1 - } - - // Mine a block to trigger the sweep. - ht.MineEmptyBlocks(1) - - // Assert the sweeping tx is found in the mempool. - htlcTimeoutOutpoint := wire.OutPoint{Hash: timeoutTx, Index: 0} - ht.AssertOutpointInMempool(htlcTimeoutOutpoint) - - // Mine a block to confirm the sweep. - ht.MineBlocksAndAssertNumTxes(1, numExpected) - - // At this point, Bob should no longer show any channels as pending - // close. - ht.AssertNumPendingForceClose(bob, 0) - - // Coop close, no anchors. - ht.CloseChannel(alice, aliceChanPoint) -} - // testMultiHopRemoteForceCloseOnChainHtlcTimeout tests that if we extend a // multi-hop HTLC, and the final destination of the HTLC force closes the // channel, then we properly timeout the HTLC directly on *their* commitment diff --git a/lntest/harness_assertion.go b/lntest/harness_assertion.go index b60985527b7..73e848c01a9 100644 --- a/lntest/harness_assertion.go +++ b/lntest/harness_assertion.go @@ -738,15 +738,19 @@ func (h *HarnessTest) AssertStreamChannelForceClosed(hn *node.HarnessNode, channeldb.ChanStatusLocalCloseInitiator.String(), "channel not coop broadcasted") + // Get the closing txid. + closeTxid, err := chainhash.NewHashFromStr(resp.ClosingTxid) + require.NoError(h, err) + // We'll now, generate a single block, wait for the final close status // update, then ensure that the closing transaction was included in the // block. - block := h.MineBlocksAndAssertNumTxes(1, 1)[0] + closeTx := h.AssertTxInMempool(*closeTxid) + h.MineBlockWithTx(closeTx) // Consume one close event and assert the closing txid can be found in // the block. closingTxid := h.WaitForChannelCloseEvent(stream) - h.AssertTxInBlock(block, closingTxid) // We should see zero waiting close channels and 1 pending force close // channels now. From d7b202524846f0315abe281be869c0f6188ffcaf Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 19 Oct 2024 10:37:33 +0800 Subject: [PATCH 74/92] lntest+itest: flatten `testMultiHopRemoteForceCloseOnChainHtlcTimeout` --- itest/list_on_test.go | 4 - itest/lnd_multi-hop_force_close_test.go | 319 ++++++++++++++++++++++++ itest/lnd_multi-hop_test.go | 211 ---------------- lntest/harness_assertion.go | 6 +- 4 files changed, 323 insertions(+), 217 deletions(-) diff --git a/itest/list_on_test.go b/itest/list_on_test.go index d11a6e5bbc7..a32278c98f5 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -149,10 +149,6 @@ var allTestCases = []*lntest.TestCase{ Name: "addpeer config", TestFunc: testAddPeerConfig, }, - { - Name: "multi hop remote force close on-chain htlc timeout", - TestFunc: testMultiHopRemoteForceCloseOnChainHtlcTimeout, - }, { Name: "private channel update policy", TestFunc: testUpdateChannelPolicyForPrivateChannel, diff --git a/itest/lnd_multi-hop_force_close_test.go b/itest/lnd_multi-hop_force_close_test.go index 49581a8b9c7..aed85da3ec4 100644 --- a/itest/lnd_multi-hop_force_close_test.go +++ b/itest/lnd_multi-hop_force_close_test.go @@ -62,6 +62,18 @@ var multiHopForceCloseTestCases = []*lntest.TestCase{ Name: "multihop local force close before timeout leased", TestFunc: testLocalForceCloseBeforeTimeoutLeased, }, + { + Name: "multihop remote force close before timeout anchor", + TestFunc: testRemoteForceCloseBeforeTimeoutAnchor, + }, + { + Name: "multihop remote force close before timeout simple taproot", + TestFunc: testRemoteForceCloseBeforeTimeoutSimpleTaproot, + }, + { + Name: "multihop remote force close before timeout leased", + TestFunc: testRemoteForceCloseBeforeTimeoutLeased, + }, } // testLocalClaimOutgoingHTLCAnchor tests `runLocalClaimOutgoingHTLC` with @@ -1089,3 +1101,310 @@ func runLocalForceCloseBeforeHtlcTimeout(ht *lntest.HarnessTest, // close. ht.AssertNumPendingForceClose(bob, 0) } + +// testRemoteForceCloseBeforeTimeoutAnchor tests +// `runRemoteForceCloseBeforeHtlcTimeout` with anchor channel. +func testRemoteForceCloseBeforeTimeoutAnchor(ht *lntest.HarnessTest) { + success := ht.Run("no zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // anchor channels. + // + // Prepare params. + params := lntest.OpenChannelParams{Amt: chanAmt} + + cfg := node.CfgAnchor + cfgCarol := append([]string{"--hodl.exit-settle"}, cfg...) + cfgs := [][]string{cfg, cfg, cfgCarol} + + runRemoteForceCloseBeforeHtlcTimeout(st, cfgs, params) + }) + if !success { + return + } + + ht.Run("zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // zero-conf anchor channels. + // + // Prepare params. + params := lntest.OpenChannelParams{ + Amt: chanAmt, + ZeroConf: true, + CommitmentType: lnrpc.CommitmentType_ANCHORS, + } + + // Prepare Carol's node config to enable zero-conf and anchor. + cfg := node.CfgZeroConf + cfgCarol := append([]string{"--hodl.exit-settle"}, cfg...) + cfgs := [][]string{cfg, cfg, cfgCarol} + + runRemoteForceCloseBeforeHtlcTimeout(st, cfgs, params) + }) +} + +// testRemoteForceCloseBeforeTimeoutSimpleTaproot tests +// `runLocalForceCloseBeforeHtlcTimeout` with simple taproot channel. +func testRemoteForceCloseBeforeTimeoutSimpleTaproot(ht *lntest.HarnessTest) { + c := lnrpc.CommitmentType_SIMPLE_TAPROOT + + success := ht.Run("no zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // simple taproot channels. + // + // Prepare params. + params := lntest.OpenChannelParams{ + Amt: chanAmt, + CommitmentType: c, + Private: true, + } + + cfg := node.CfgSimpleTaproot + cfgCarol := append([]string{"--hodl.exit-settle"}, cfg...) + cfgs := [][]string{cfg, cfg, cfgCarol} + + runRemoteForceCloseBeforeHtlcTimeout(st, cfgs, params) + }) + if !success { + return + } + + ht.Run("zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // zero-conf simple taproot channels. + // + // Prepare params. + params := lntest.OpenChannelParams{ + Amt: chanAmt, + ZeroConf: true, + CommitmentType: c, + Private: true, + } + + // Prepare Carol's node config to enable zero-conf and leased + // channel. + cfg := node.CfgSimpleTaproot + cfg = append(cfg, node.CfgZeroConf...) + cfgCarol := append([]string{"--hodl.exit-settle"}, cfg...) + cfgs := [][]string{cfg, cfg, cfgCarol} + + runRemoteForceCloseBeforeHtlcTimeout(st, cfgs, params) + }) +} + +// testRemoteForceCloseBeforeTimeoutLeased tests +// `runRemoteForceCloseBeforeHtlcTimeout` with script enforced lease channel. +func testRemoteForceCloseBeforeTimeoutLeased(ht *lntest.HarnessTest) { + success := ht.Run("no zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // leased channels. + // + // Prepare params. + params := lntest.OpenChannelParams{ + Amt: chanAmt, + CommitmentType: leasedType, + } + + cfg := node.CfgLeased + cfgCarol := append([]string{"--hodl.exit-settle"}, cfg...) + cfgs := [][]string{cfg, cfg, cfgCarol} + + runRemoteForceCloseBeforeHtlcTimeout(st, cfgs, params) + }) + if !success { + return + } + + ht.Run("zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // zero-conf anchor channels. + // + // Prepare params. + params := lntest.OpenChannelParams{ + Amt: chanAmt, + ZeroConf: true, + CommitmentType: leasedType, + } + + // Prepare Carol's node config to enable zero-conf and leased + // channel. + cfg := node.CfgLeased + cfg = append(cfg, node.CfgZeroConf...) + cfgCarol := append([]string{"--hodl.exit-settle"}, cfg...) + cfgs := [][]string{cfg, cfg, cfgCarol} + + runRemoteForceCloseBeforeHtlcTimeout(st, cfgs, params) + }) +} + +// runRemoteForceCloseBeforeHtlcTimeout tests that if we extend a multi-hop +// HTLC, and the final destination of the HTLC force closes the channel, then +// we properly timeout the HTLC directly on *their* commitment transaction once +// the timeout has expired. Once we sweep the transaction, we should also +// cancel back the initial HTLC. +func runRemoteForceCloseBeforeHtlcTimeout(ht *lntest.HarnessTest, + cfgs [][]string, params lntest.OpenChannelParams) { + + // Set the min relay feerate to be 10 sat/vbyte so the non-CPFP anchor + // is never swept. + // + // TODO(yy): delete this line once the normal anchor sweeping is + // removed. + ht.SetMinRelayFeerate(10_000) + + // Create a three hop network: Alice -> Bob -> Carol. + chanPoints, nodes := ht.CreateSimpleNetwork(cfgs, params) + alice, bob, carol := nodes[0], nodes[1], nodes[2] + bobChanPoint := chanPoints[1] + + // If this is a taproot channel, then we'll need to make some manual + // route hints so Alice can actually find a route. + var routeHints []*lnrpc.RouteHint + if params.CommitmentType == lnrpc.CommitmentType_SIMPLE_TAPROOT { + routeHints = makeRouteHints(bob, carol, params.ZeroConf) + } + + // With our channels set up, we'll then send a single HTLC from Alice + // to Carol. As Carol is in hodl mode, she won't settle this HTLC which + // opens up the base for out tests. + var preimage lntypes.Preimage + copy(preimage[:], ht.Random32Bytes()) + payHash := preimage.Hash() + invoiceReq := &invoicesrpc.AddHoldInvoiceRequest{ + Value: int64(htlcAmt), + CltvExpiry: finalCltvDelta, + Hash: payHash[:], + RouteHints: routeHints, + } + carolInvoice := carol.RPC.AddHoldInvoice(invoiceReq) + + // Subscribe the invoice. + stream := carol.RPC.SubscribeSingleInvoice(payHash[:]) + + req := &routerrpc.SendPaymentRequest{ + PaymentRequest: carolInvoice.PaymentRequest, + TimeoutSeconds: 60, + FeeLimitMsat: noFeeLimitMsat, + } + alice.RPC.SendPayment(req) + + // Once the HTLC has cleared, all the nodes in our mini network should + // show that the HTLC has been locked in. + ht.AssertActiveHtlcs(alice, payHash[:]) + ht.AssertActiveHtlcs(bob, payHash[:]) + ht.AssertActiveHtlcs(carol, payHash[:]) + + // At this point, we'll now instruct Carol to force close the tx. This + // will let us exercise that Bob is able to sweep the expired HTLC on + // Carol's version of the commitment tx. + closeStream, _ := ht.CloseChannelAssertPending( + carol, bobChanPoint, true, + ) + + // For anchor channels, the anchor won't be used for CPFP because + // channel arbitrator thinks Carol doesn't have preimage for her + // incoming HTLC on the commitment transaction Bob->Carol. Although + // Carol created this invoice, because it's a hold invoice, the + // preimage won't be generated automatically. + ht.AssertStreamChannelForceClosed( + carol, bobChanPoint, true, closeStream, + ) + + // At this point, Bob should have a pending force close channel as + // Carol has gone directly to chain. + ht.AssertNumPendingForceClose(bob, 1) + + // Carol will offer her anchor to her sweeper. + ht.AssertNumPendingSweeps(carol, 1) + + // Bob should offered the anchor output to his sweeper. + if params.CommitmentType == leasedType { + // For script enforced lease channels, Bob can sweep his anchor + // output immediately although it will be skipped due to it + // being uneconomical. His to_local output is CLTV locked so it + // cannot be swept yet. + ht.AssertNumPendingSweeps(bob, 1) + } else { + // For non-leased channels, Bob can sweep his commit and anchor + // outputs immediately. + ht.AssertNumPendingSweeps(bob, 2) + + // We expect to see only one sweeping tx to be published from + // Bob, which sweeps his to_local output. His anchor output + // won't be swept due it being uneconomical. For Carol, since + // her anchor is not used for CPFP, it'd be also uneconomical + // to sweep so it will fail. + ht.MineBlocksAndAssertNumTxes(1, 1) + } + + // Next, we'll mine enough blocks for the HTLC to expire. At this + // point, Bob should hand off the output to his sweeper, which will + // broadcast a sweep transaction. + resp := ht.AssertNumPendingForceClose(bob, 1)[0] + require.Equal(ht, 1, len(resp.PendingHtlcs)) + + ht.Logf("Bob's timelock to_local output=%v, timelock on second stage "+ + "htlc=%v", resp.BlocksTilMaturity, + resp.PendingHtlcs[0].BlocksTilMaturity) + + ht.MineBlocks(int(resp.PendingHtlcs[0].BlocksTilMaturity)) + + // If we check Bob's pending channel report, it should show that he has + // a single HTLC that's now in the second stage, as it skipped the + // initial first stage since this is a direct HTLC. + ht.AssertNumHTLCsAndStage(bob, bobChanPoint, 1, 2) + + // Bob should have two pending sweep requests, + // 1. the uneconomical anchor sweep. + // 2. the direct timeout sweep. + ht.AssertNumPendingSweeps(bob, 2) + + // Bob's sweeping tx should now be found in the mempool. + sweepTx := ht.AssertNumTxsInMempool(1)[0] + + // If we mine an additional block, then this should confirm Bob's tx + // which sweeps the direct HTLC output. + block := ht.MineBlocksAndAssertNumTxes(1, 1)[0] + ht.AssertTxInBlock(block, sweepTx) + + // Now that the sweeping tx has been confirmed, Bob should cancel back + // that HTLC. As a result, Alice should not know of any active HTLC's. + ht.AssertNumActiveHtlcs(alice, 0) + + // For script enforced lease channels, Bob still need to wait for the + // CLTV lock to expire before he can sweep his to_local output. + if params.CommitmentType == leasedType { + // Get the remaining blocks to mine. + resp = ht.AssertNumPendingForceClose(bob, 1)[0] + ht.MineBlocks(int(resp.BlocksTilMaturity)) + + // Assert the commit output has been offered to the sweeper. + // Bob should have two pending sweep requests - one for the + // commit output and one for the anchor output. + ht.AssertNumPendingSweeps(bob, 2) + + // Mine the to_local sweep tx. + ht.MineBlocksAndAssertNumTxes(1, 1) + } + + // Now we'll check Bob's pending channel report. Since this was Carol's + // commitment, he doesn't have to wait for any CSV delays, but he may + // still need to wait for a CLTV on his commit output to expire + // depending on the commitment type. + ht.AssertNumPendingForceClose(bob, 0) + + // While we're here, we assert that our expired invoice's state is + // correctly updated, and can no longer be settled. + ht.AssertInvoiceState(stream, lnrpc.Invoice_CANCELED) +} diff --git a/itest/lnd_multi-hop_test.go b/itest/lnd_multi-hop_test.go index 2a866f7929b..e6437b053bd 100644 --- a/itest/lnd_multi-hop_test.go +++ b/itest/lnd_multi-hop_test.go @@ -160,217 +160,6 @@ func runMultiHopHtlcClaimTest(ht *lntest.HarnessTest, tester caseRunner) { } } -// testMultiHopRemoteForceCloseOnChainHtlcTimeout tests that if we extend a -// multi-hop HTLC, and the final destination of the HTLC force closes the -// channel, then we properly timeout the HTLC directly on *their* commitment -// transaction once the timeout has expired. Once we sweep the transaction, we -// should also cancel back the initial HTLC. -func testMultiHopRemoteForceCloseOnChainHtlcTimeout(ht *lntest.HarnessTest) { - runMultiHopHtlcClaimTest( - ht, runMultiHopRemoteForceCloseOnChainHtlcTimeout, - ) -} - -func runMultiHopRemoteForceCloseOnChainHtlcTimeout(ht *lntest.HarnessTest, - alice, bob *node.HarnessNode, c lnrpc.CommitmentType, zeroConf bool) { - - // First, we'll create a three hop network: Alice -> Bob -> Carol, with - // Carol refusing to actually settle or directly cancel any HTLC's - // self. - aliceChanPoint, bobChanPoint, carol := createThreeHopNetwork( - ht, alice, bob, true, c, zeroConf, - ) - - // With our channels set up, we'll then send a single HTLC from Alice - // to Carol. As Carol is in hodl mode, she won't settle this HTLC which - // opens up the base for out tests. - const htlcAmt = btcutil.Amount(30000) - - // If this is a taproot channel, then we'll need to make some manual - // route hints so Alice can actually find a route. - var routeHints []*lnrpc.RouteHint - if c == lnrpc.CommitmentType_SIMPLE_TAPROOT { - routeHints = makeRouteHints(bob, carol, zeroConf) - } - - // We'll now send a single HTLC across our multi-hop network. - var preimage lntypes.Preimage - copy(preimage[:], ht.Random32Bytes()) - payHash := preimage.Hash() - invoiceReq := &invoicesrpc.AddHoldInvoiceRequest{ - Value: int64(htlcAmt), - CltvExpiry: finalCltvDelta, - Hash: payHash[:], - RouteHints: routeHints, - } - carolInvoice := carol.RPC.AddHoldInvoice(invoiceReq) - - // Subscribe the invoice. - stream := carol.RPC.SubscribeSingleInvoice(payHash[:]) - - req := &routerrpc.SendPaymentRequest{ - PaymentRequest: carolInvoice.PaymentRequest, - TimeoutSeconds: 60, - FeeLimitMsat: noFeeLimitMsat, - } - alice.RPC.SendPayment(req) - - // blocksMined records how many blocks have mined after the creation of - // the invoice so it can be used to calculate how many more blocks need - // to be mined to trigger a force close later on. - var blocksMined uint32 - - // Once the HTLC has cleared, all the nodes in our mini network should - // show that the HTLC has been locked in. - ht.AssertActiveHtlcs(alice, payHash[:]) - ht.AssertActiveHtlcs(bob, payHash[:]) - ht.AssertActiveHtlcs(carol, payHash[:]) - - // At this point, we'll now instruct Carol to force close the - // transaction. This will let us exercise that Bob is able to sweep the - // expired HTLC on Carol's version of the commitment transaction. - closeStream, _ := ht.CloseChannelAssertPending( - carol, bobChanPoint, true, - ) - - // For anchor channels, the anchor won't be used for CPFP because - // channel arbitrator thinks Carol doesn't have preimage for her - // incoming HTLC on the commitment transaction Bob->Carol. Although - // Carol created this invoice, because it's a hold invoice, the - // preimage won't be generated automatically. - closeTx := ht.AssertStreamChannelForceClosed( - carol, bobChanPoint, true, closeStream, - ) - - // Increase the blocks mined. At this step - // AssertStreamChannelForceClosed mines one block. - blocksMined++ - - // At this point, Bob should have a pending force close channel as - // Carol has gone directly to chain. - ht.AssertNumPendingForceClose(bob, 1) - - var expectedTxes int - switch c { - // Bob can sweep his commit and anchor outputs immediately. Carol will - // also offer her anchor to her sweeper. - case lnrpc.CommitmentType_ANCHORS, lnrpc.CommitmentType_SIMPLE_TAPROOT: - ht.AssertNumPendingSweeps(bob, 2) - ht.AssertNumPendingSweeps(carol, 1) - - // We expect to see only one sweeping tx to be published from - // Bob, which sweeps his commit and anchor outputs in the same - // tx. For Carol, since her anchor is not used for CPFP, it'd - // be uneconomical to sweep so it will fail. - expectedTxes = 1 - - // Bob can't sweep his commit output yet as he was the initiator of a - // script-enforced leased channel, so he'll always incur the additional - // CLTV. He can still offer his anchor output to his sweeper however. - case lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE: - ht.AssertNumPendingSweeps(bob, 1) - ht.AssertNumPendingSweeps(carol, 1) - - // We expect to see only no sweeping txns to be published, - // neither Bob's or Carol's anchor sweep can succeed due to - // it's uneconomical. - expectedTxes = 0 - - default: - ht.Fatalf("unhandled commitment type %v", c) - } - - // Mine one block to trigger the sweeps. - ht.MineEmptyBlocks(1) - blocksMined++ - - // We now mine a block to clear up the mempool. - ht.MineBlocksAndAssertNumTxes(1, expectedTxes) - blocksMined++ - - // Next, we'll mine enough blocks for the HTLC to expire. At this - // point, Bob should hand off the output to his internal utxo nursery, - // which will broadcast a sweep transaction. - numBlocks := padCLTV(uint32(finalCltvDelta) - - lncfg.DefaultOutgoingBroadcastDelta) - ht.MineEmptyBlocks(int(numBlocks - blocksMined)) - - // If we check Bob's pending channel report, it should show that he has - // a single HTLC that's now in the second stage, as it skipped the - // initial first stage since this is a direct HTLC. - ht.AssertNumHTLCsAndStage(bob, bobChanPoint, 1, 2) - - // We need to generate an additional block to expire the CSV 1. - ht.MineEmptyBlocks(1) - - // For script-enforced leased channels, Bob has failed to sweep his - // anchor output before, so it's still pending. - if c == lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE { - ht.AssertNumPendingSweeps(bob, 2) - } else { - // Bob should have a pending sweep request. - ht.AssertNumPendingSweeps(bob, 1) - } - - // Mine a block to trigger the sweeper to sweep it. - ht.MineEmptyBlocks(1) - - // Bob's sweeping transaction should now be found in the mempool at - // this point. - sweepTx := ht.AssertNumTxsInMempool(1)[0] - - // If we mine an additional block, then this should confirm Bob's - // transaction which sweeps the direct HTLC output. - block := ht.MineBlocksAndAssertNumTxes(1, 1)[0] - ht.AssertTxInBlock(block, sweepTx) - - // Now that the sweeping transaction has been confirmed, Bob should - // cancel back that HTLC. As a result, Alice should not know of any - // active HTLC's. - ht.AssertNumActiveHtlcs(alice, 0) - - // Now we'll check Bob's pending channel report. Since this was Carol's - // commitment, he doesn't have to wait for any CSV delays, but he may - // still need to wait for a CLTV on his commit output to expire - // depending on the commitment type. - if c == lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE { - resp := bob.RPC.PendingChannels() - - require.Len(ht, resp.PendingForceClosingChannels, 1) - forceCloseChan := resp.PendingForceClosingChannels[0] - require.Positive(ht, forceCloseChan.BlocksTilMaturity) - - numBlocks := int(forceCloseChan.BlocksTilMaturity) - ht.MineEmptyBlocks(numBlocks) - - // Assert the commit output has been offered to the sweeper. - // Bob should have two pending sweep requests - one for the - // commit output and one for the anchor output. - ht.AssertNumPendingSweeps(bob, 2) - - // Mine a block to trigger the sweep. - ht.MineEmptyBlocks(1) - - bobCommitOutpoint := wire.OutPoint{Hash: closeTx, Index: 3} - bobCommitSweep := ht.AssertOutpointInMempool( - bobCommitOutpoint, - ) - bobCommitSweepTxid := bobCommitSweep.TxHash() - block := ht.MineBlocksAndAssertNumTxes(1, 1)[0] - ht.AssertTxInBlock(block, bobCommitSweepTxid) - } - ht.AssertNumPendingForceClose(bob, 0) - - // While we're here, we assert that our expired invoice's state is - // correctly updated, and can no longer be settled. - ht.AssertInvoiceState(stream, lnrpc.Invoice_CANCELED) - - // We'll close out the test by closing the channel from Alice to Bob, - // and then shutting down the new node we created as its no longer - // needed. Coop close, no anchors. - ht.CloseChannel(alice, aliceChanPoint) -} - // testMultiHopHtlcLocalChainClaim tests that in a multi-hop HTLC scenario, if // we force close a channel with an incoming HTLC, and later find out the // preimage via the witness beacon, we properly settle the HTLC on-chain using diff --git a/lntest/harness_assertion.go b/lntest/harness_assertion.go index 73e848c01a9..1d081bc1260 100644 --- a/lntest/harness_assertion.go +++ b/lntest/harness_assertion.go @@ -29,6 +29,7 @@ import ( "github.com/lightningnetwork/lnd/lntest/rpc" "github.com/lightningnetwork/lnd/lntest/wait" "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnutils" "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" ) @@ -1613,8 +1614,9 @@ func (h *HarnessTest) AssertNumHTLCsAndStage(hn *node.HarnessNode, } if len(target.PendingHtlcs) != num { - return fmt.Errorf("got %d pending htlcs, want %d", - len(target.PendingHtlcs), num) + return fmt.Errorf("got %d pending htlcs, want %d, %s", + len(target.PendingHtlcs), num, + lnutils.SpewLogClosure(target.PendingHtlcs)()) } for i, htlc := range target.PendingHtlcs { From 8dd73a08a9ce5e5f5989925c0ffca33b912c5f12 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Mon, 21 Oct 2024 13:05:41 +0800 Subject: [PATCH 75/92] itest: flatten `testMultiHopHtlcLocalChainClaim` --- itest/list_on_test.go | 4 - itest/lnd_multi-hop_force_close_test.go | 680 +++++++++++++++++++++++- itest/lnd_multi-hop_test.go | 361 ------------- 3 files changed, 675 insertions(+), 370 deletions(-) diff --git a/itest/list_on_test.go b/itest/list_on_test.go index a32278c98f5..f471e777f3d 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -297,10 +297,6 @@ var allTestCases = []*lntest.TestCase{ Name: "REST API", TestFunc: testRestAPI, }, - { - Name: "multi hop htlc local chain claim", - TestFunc: testMultiHopHtlcLocalChainClaim, - }, { Name: "multi hop htlc remote chain claim", TestFunc: testMultiHopHtlcRemoteChainClaim, diff --git a/itest/lnd_multi-hop_force_close_test.go b/itest/lnd_multi-hop_force_close_test.go index aed85da3ec4..08d2b7d6e7d 100644 --- a/itest/lnd_multi-hop_force_close_test.go +++ b/itest/lnd_multi-hop_force_close_test.go @@ -15,8 +15,11 @@ import ( ) const ( - chanAmt = 1000000 - htlcAmt = btcutil.Amount(300_000) + chanAmt = 1_000_000 + invoiceAmt = 100_000 + htlcAmt = btcutil.Amount(300_000) + + incomingBroadcastDelta = lncfg.DefaultIncomingBroadcastDelta ) var leasedType = lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE @@ -74,6 +77,18 @@ var multiHopForceCloseTestCases = []*lntest.TestCase{ Name: "multihop remote force close before timeout leased", TestFunc: testRemoteForceCloseBeforeTimeoutLeased, }, + { + Name: "multihop local claim incoming htlc anchor", + TestFunc: testLocalClaimIncomingHTLCAnchor, + }, + { + Name: "multihop local claim incoming htlc simple taproot", + TestFunc: testLocalClaimIncomingHTLCSimpleTaproot, + }, + { + Name: "multihop local claim incoming htlc leased", + TestFunc: testLocalClaimIncomingHTLCLeased, + }, } // testLocalClaimOutgoingHTLCAnchor tests `runLocalClaimOutgoingHTLC` with @@ -582,8 +597,6 @@ func runMultiHopReceiverPreimageClaim(ht *lntest.HarnessTest, // With the network active, we'll now add a new hodl invoice at Carol's // end. Make sure the cltv expiry delta is large enough, otherwise Bob // won't send out the outgoing htlc. - const invoiceAmt = 100000 - var preimage lntypes.Preimage copy(preimage[:], ht.Random32Bytes()) payHash := preimage.Hash() @@ -634,7 +647,7 @@ func runMultiHopReceiverPreimageClaim(ht *lntest.HarnessTest, // close her channel with Bob, broadcast the closing tx but keep it // unconfirmed. numBlocks := padCLTV(uint32( - invoiceReq.CltvExpiry - lncfg.DefaultIncomingBroadcastDelta, + invoiceReq.CltvExpiry - incomingBroadcastDelta, )) // Now we'll mine enough blocks to prompt Carol to actually go to the @@ -1408,3 +1421,660 @@ func runRemoteForceCloseBeforeHtlcTimeout(ht *lntest.HarnessTest, // correctly updated, and can no longer be settled. ht.AssertInvoiceState(stream, lnrpc.Invoice_CANCELED) } + +// testLocalClaimIncomingHTLCAnchor tests `runLocalClaimIncomingHTLC` with +// anchor channel. +func testLocalClaimIncomingHTLCAnchor(ht *lntest.HarnessTest) { + success := ht.Run("no zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // anchor channels. + // + // Prepare params. + params := lntest.OpenChannelParams{Amt: chanAmt} + + cfg := node.CfgAnchor + cfgs := [][]string{cfg, cfg, cfg} + + runLocalClaimIncomingHTLC(st, cfgs, params) + }) + if !success { + return + } + + ht.Run("zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // zero-conf anchor channels. + // + // Prepare params. + params := lntest.OpenChannelParams{ + Amt: chanAmt, + ZeroConf: true, + CommitmentType: lnrpc.CommitmentType_ANCHORS, + } + + // Prepare Carol's node config to enable zero-conf and anchor. + cfg := node.CfgZeroConf + cfgs := [][]string{cfg, cfg, cfg} + + runLocalClaimIncomingHTLC(st, cfgs, params) + }) +} + +// testLocalClaimIncomingHTLCSimpleTaproot tests `runLocalClaimIncomingHTLC` +// with simple taproot channel. +func testLocalClaimIncomingHTLCSimpleTaproot(ht *lntest.HarnessTest) { + c := lnrpc.CommitmentType_SIMPLE_TAPROOT + + success := ht.Run("no zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // simple taproot channels. + // + // Prepare params. + params := lntest.OpenChannelParams{ + Amt: chanAmt, + CommitmentType: c, + Private: true, + } + + cfg := node.CfgSimpleTaproot + cfgs := [][]string{cfg, cfg, cfg} + + runLocalClaimIncomingHTLC(st, cfgs, params) + }) + if !success { + return + } + + ht.Run("zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // zero-conf simple taproot channels. + // + // Prepare params. + params := lntest.OpenChannelParams{ + Amt: chanAmt, + ZeroConf: true, + CommitmentType: c, + Private: true, + } + + // Prepare Carol's node config to enable zero-conf and leased + // channel. + cfg := node.CfgSimpleTaproot + cfg = append(cfg, node.CfgZeroConf...) + cfgs := [][]string{cfg, cfg, cfg} + + runLocalClaimIncomingHTLC(st, cfgs, params) + }) +} + +// runLocalClaimIncomingHTLC tests that in a multi-hop HTLC scenario, if we +// force close a channel with an incoming HTLC, and later find out the preimage +// via the witness beacon, we properly settle the HTLC on-chain using the HTLC +// success transaction in order to ensure we don't lose any funds. +func runLocalClaimIncomingHTLC(ht *lntest.HarnessTest, + cfgs [][]string, params lntest.OpenChannelParams) { + + // Set the min relay feerate to be 10 sat/vbyte so the non-CPFP anchor + // is never swept. + // + // TODO(yy): delete this line once the normal anchor sweeping is + // removed. + ht.SetMinRelayFeerate(10_000) + + // Create a three hop network: Alice -> Bob -> Carol. + chanPoints, nodes := ht.CreateSimpleNetwork(cfgs, params) + alice, bob, carol := nodes[0], nodes[1], nodes[2] + aliceChanPoint := chanPoints[0] + + // Fund Carol one UTXO so she can sweep outputs. + ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) + + // If this is a taproot channel, then we'll need to make some manual + // route hints so Alice can actually find a route. + var routeHints []*lnrpc.RouteHint + if params.CommitmentType == lnrpc.CommitmentType_SIMPLE_TAPROOT { + routeHints = makeRouteHints(bob, carol, params.ZeroConf) + } + + // With the network active, we'll now add a new hodl invoice at Carol's + // end. Make sure the cltv expiry delta is large enough, otherwise Bob + // won't send out the outgoing htlc. + preimage := ht.RandomPreimage() + payHash := preimage.Hash() + + invoiceReq := &invoicesrpc.AddHoldInvoiceRequest{ + Value: invoiceAmt, + CltvExpiry: finalCltvDelta, + Hash: payHash[:], + RouteHints: routeHints, + } + carolInvoice := carol.RPC.AddHoldInvoice(invoiceReq) + + // Subscribe the invoice. + stream := carol.RPC.SubscribeSingleInvoice(payHash[:]) + + // Now that we've created the invoice, we'll send a single payment from + // Alice to Carol. We won't wait for the response however, as Carol + // will not immediately settle the payment. + req := &routerrpc.SendPaymentRequest{ + PaymentRequest: carolInvoice.PaymentRequest, + TimeoutSeconds: 60, + FeeLimitMsat: noFeeLimitMsat, + } + alice.RPC.SendPayment(req) + + // At this point, all 3 nodes should now have an active channel with + // the created HTLC pending on all of them. + ht.AssertActiveHtlcs(alice, payHash[:]) + ht.AssertActiveHtlcs(bob, payHash[:]) + ht.AssertActiveHtlcs(carol, payHash[:]) + + // Wait for carol to mark invoice as accepted. There is a small gap to + // bridge between adding the htlc to the channel and executing the exit + // hop logic. + ht.AssertInvoiceState(stream, lnrpc.Invoice_ACCEPTED) + + // At this point, Bob decides that he wants to exit the channel + // Alice=>Bob immediately, so he force closes his commitment tx. + closeStream, _ := ht.CloseChannelAssertPending( + bob, aliceChanPoint, true, + ) + + // For anchor channels, the anchor won't be used for CPFP as there's no + // deadline pressure for Bob on the channel Alice->Bob at the moment. + // For Bob's local commitment tx, there's only one incoming HTLC which + // he doesn't have the preimage yet. + hasAnchorSweep := false + bobForceClose := ht.AssertStreamChannelForceClosed( + bob, aliceChanPoint, hasAnchorSweep, closeStream, + ) + + // Alice will offer her to_local and anchor outputs to her sweeper. + ht.AssertNumPendingSweeps(alice, 2) + + // Bob will offer his anchor to his sweeper. + ht.AssertNumPendingSweeps(bob, 1) + + // Assert the expected num of txns are found in the mempool. + // + // We expect to see only one sweeping tx to be published from Alice, + // which sweeps her to_local output (which is to to_remote on Bob's + // commit tx). Her anchor output won't be swept as it's uneconomical. + // For Bob, since his anchor is not used for CPFP, it'd be uneconomical + // to sweep so it will fail. + ht.AssertNumTxsInMempool(1) + + // Mine a block to confirm Alice's sweeping tx. + ht.MineBlocksAndAssertNumTxes(1, 1) + + // Suspend Bob to force Carol to go to chain. + restartBob := ht.SuspendNode(bob) + + // Settle invoice. This will just mark the invoice as settled, as there + // is no link anymore to remove the htlc from the commitment tx. For + // this test, it is important to actually settle and not leave the + // invoice in the accepted state, because without a known preimage, the + // channel arbitrator won't go to chain. + carol.RPC.SettleInvoice(preimage[:]) + + // We now advance the block height to the point where Carol will force + // close her channel with Bob, broadcast the closing tx but keep it + // unconfirmed. + numBlocks := padCLTV( + uint32(invoiceReq.CltvExpiry - incomingBroadcastDelta), + ) + + // We've already mined 2 blocks at this point, so we only need to mine + // CLTV-2 blocks. + ht.MineBlocks(int(numBlocks - 2)) + + // Expect two txns in the mempool, + // - Carol's force close tx. + // - Carol's CPFP anchor sweeping tx. + // Mine a block to confirm them. + ht.MineBlocksAndAssertNumTxes(1, 2) + + // After the force close tx is mined, Carol should offer her + // second-level success HTLC tx to her sweeper. + ht.AssertNumPendingSweeps(carol, 1) + + // Restart bob again. + require.NoError(ht, restartBob()) + + // Once Bob is online and sees the force close tx Bob=>Carol, he will + // create a tx to sweep his commitment output. His anchor outputs will + // not be swept due to uneconomical. We expect to see three sweeping + // requests, + // - the commitment output. + // - the anchor output from channel Alice=>Bob. + // - the anchor output from channel Bob=>Carol. + ht.AssertNumPendingSweeps(bob, 3) + + // Mine an empty block the for neutrino backend. We need this step to + // trigger Bob's chain watcher to detect the force close tx. Deep down, + // this happens because the notification system for neutrino is very + // different from others. Specifically, when a block contains the force + // close tx is notified, these two calls, + // - RegisterBlockEpochNtfn, will notify the block first. + // - RegisterSpendNtfn, will wait for the neutrino notifier to sync to + // the block, then perform a GetUtxo, which, by the time the spend + // details are sent, the blockbeat is considered processed in Bob's + // chain watcher. + // + // TODO(yy): refactor txNotifier to fix the above issue. + if ht.IsNeutrinoBackend() { + ht.MineEmptyBlocks(1) + } + + // Assert txns can be found in the mempool. + // + // Carol will broadcast her sweeping tx and Bob will sweep his + // commitment anchor output, we'd expect to see two txns, + // - Carol's second level HTLC tx. + // - Bob's commitment output sweeping tx. + ht.AssertNumTxsInMempool(2) + + // At this point we suspend Alice to make sure she'll handle the + // on-chain settle after a restart. + restartAlice := ht.SuspendNode(alice) + + // Mine a block to confirm the sweeping txns made by Bob and Carol. + ht.MineBlocksAndAssertNumTxes(1, 2) + + // When Bob notices Carol's second level tx in the block, he will + // extract the preimage and broadcast a second level tx to claim the + // HTLC in his (already closed) channel with Alice, which means Bob has + // three sweeping requests, + // - the second level HTLC tx from channel Alice=>Bob. + // - the anchor output from channel Alice=>Bob. + // - the anchor output from channel Bob=>Carol. + ht.AssertNumPendingSweeps(bob, 3) + + // Mine a block to trigger the sweep. This is needed because the + // preimage extraction logic from the link is not managed by the + // blockbeat, which means the preimage may be sent to the contest + // resolver after it's launched. + // + // TODO(yy): Expose blockbeat to the link layer. + ht.MineEmptyBlocks(1) + + // At this point, Bob should have broadcast his second layer success + // tx, and should have sent it to his sweeper. + // + // Check Bob's second level tx. + bobSecondLvlTx := ht.GetNumTxsFromMempool(1)[0] + + // It should spend from the commitment in the channel with Alice. + ht.AssertTxSpendFrom(bobSecondLvlTx, bobForceClose) + + // We'll now mine a block which should confirm Bob's second layer tx. + ht.MineBlocksAndAssertNumTxes(1, 1) + + // Bob should consider the channel Bob=>Carol closed, and channel + // Alice=>Bob pending close. + ht.AssertNumPendingForceClose(bob, 1) + + // Now that the preimage from Bob has hit the chain, restart Alice to + // ensure she'll pick it up. + require.NoError(ht, restartAlice()) + + // If we then mine 1 additional block, Carol's second level tx should + // mature, and she can pull the funds from it with a sweep tx. + resp := ht.AssertNumPendingForceClose(carol, 1)[0] + require.Equal(ht, 1, len(resp.PendingHtlcs)) + + ht.Logf("Carol's timelock to_local output=%v, timelock on second "+ + "stage htlc=%v", resp.BlocksTilMaturity, + resp.PendingHtlcs[0].BlocksTilMaturity) + + ht.MineBlocks(int(resp.PendingHtlcs[0].BlocksTilMaturity)) + + // Carol should have one a sweep request for her second level tx. + ht.AssertNumPendingSweeps(carol, 1) + + // Carol's sweep tx should be broadcast, assert it's in the mempool and + // mine it. + ht.MineBlocksAndAssertNumTxes(1, 1) + + // We now mine blocks till the CSV lock on Bob's success HTLC on + // commitment Alice=>Bob expires. + resp = ht.AssertNumPendingForceClose(bob, 1)[0] + require.Equal(ht, 1, len(resp.PendingHtlcs)) + + ht.Logf("Bob's timelock to_local output=%v, timelock on second stage "+ + "htlc=%v", resp.BlocksTilMaturity, + resp.PendingHtlcs[0].BlocksTilMaturity) + + ht.MineBlocks(int(resp.PendingHtlcs[0].BlocksTilMaturity)) + + // Bob should have three requests in his sweeper. + // - the second level HTLC tx. + // - the anchor output from channel Alice=>Bob. + // - the anchor output from channel Bob=>Carol. + ht.AssertNumPendingSweeps(bob, 3) + + // When we mine one additional block, that will confirm Bob's sweep. + // Now Bob should have no pending channels anymore, as this just + // resolved it by the confirmation of the sweep transaction. + ht.MineBlocksAndAssertNumTxes(1, 1) + + // All nodes should show zero pending and open channels. + for _, node := range []*node.HarnessNode{alice, bob, carol} { + ht.AssertNumPendingForceClose(node, 0) + ht.AssertNodeNumChannels(node, 0) + } + + // Finally, check that the Alice's payment is correctly marked + // succeeded. + ht.AssertPaymentStatus(alice, preimage, lnrpc.Payment_SUCCEEDED) +} + +// testLocalClaimIncomingHTLCLeased tests `runLocalClaimIncomingHTLCLeased` +// with script enforced lease channel. +func testLocalClaimIncomingHTLCLeased(ht *lntest.HarnessTest) { + success := ht.Run("no zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // leased channels. + // + // Prepare params. + params := lntest.OpenChannelParams{ + Amt: chanAmt, + CommitmentType: leasedType, + } + + cfg := node.CfgLeased + cfgs := [][]string{cfg, cfg, cfg} + + runLocalClaimIncomingHTLCLeased(st, cfgs, params) + }) + if !success { + return + } + + ht.Run("zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // zero-conf anchor channels. + // + // Prepare params. + params := lntest.OpenChannelParams{ + Amt: chanAmt, + ZeroConf: true, + CommitmentType: leasedType, + } + + // Prepare Carol's node config to enable zero-conf and leased + // channel. + cfg := node.CfgLeased + cfg = append(cfg, node.CfgZeroConf...) + cfgs := [][]string{cfg, cfg, cfg} + + runLocalClaimIncomingHTLCLeased(st, cfgs, params) + }) +} + +// runLocalClaimIncomingHTLCLeased tests that in a multi-hop HTLC scenario, if +// we force close a channel with an incoming HTLC, and later find out the +// preimage via the witness beacon, we properly settle the HTLC on-chain using +// the HTLC success transaction in order to ensure we don't lose any funds. +// +// TODO(yy): simplify or remove this test as it's too complicated. +func runLocalClaimIncomingHTLCLeased(ht *lntest.HarnessTest, + cfgs [][]string, params lntest.OpenChannelParams) { + + // Set the min relay feerate to be 5 sat/vbyte so the non-CPFP anchor + // is never swept. + // + // TODO(yy): delete this line once the normal anchor sweeping is + // removed. + ht.SetMinRelayFeerate(5000) + + // Create a three hop network: Alice -> Bob -> Carol. + chanPoints, nodes := ht.CreateSimpleNetwork(cfgs, params) + alice, bob, carol := nodes[0], nodes[1], nodes[2] + aliceChanPoint, bobChanPoint := chanPoints[0], chanPoints[1] + + // Fund Carol one UTXO so she can sweep outputs. + ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) + + // Carol should have enough wallet UTXOs here to sweep the HTLC in the + // end of this test. However, due to a known issue, Carol's wallet may + // report there's no UTXO available. For details, + // - https://github.com/lightningnetwork/lnd/issues/8786 + // + // TODO(yy): remove this step once the issue is resolved. + ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) + + // With the network active, we'll now add a new hodl invoice at Carol's + // end. Make sure the cltv expiry delta is large enough, otherwise Bob + // won't send out the outgoing htlc. + preimage := ht.RandomPreimage() + payHash := preimage.Hash() + + invoiceReq := &invoicesrpc.AddHoldInvoiceRequest{ + Value: invoiceAmt, + CltvExpiry: finalCltvDelta, + Hash: payHash[:], + } + carolInvoice := carol.RPC.AddHoldInvoice(invoiceReq) + + // Subscribe the invoice. + stream := carol.RPC.SubscribeSingleInvoice(payHash[:]) + + // Now that we've created the invoice, we'll send a single payment from + // Alice to Carol. We won't wait for the response however, as Carol + // will not immediately settle the payment. + req := &routerrpc.SendPaymentRequest{ + PaymentRequest: carolInvoice.PaymentRequest, + TimeoutSeconds: 60, + FeeLimitMsat: noFeeLimitMsat, + } + alice.RPC.SendPayment(req) + + // At this point, all 3 nodes should now have an active channel with + // the created HTLC pending on all of them. + ht.AssertActiveHtlcs(alice, payHash[:]) + ht.AssertActiveHtlcs(bob, payHash[:]) + ht.AssertActiveHtlcs(carol, payHash[:]) + + // Wait for carol to mark invoice as accepted. There is a small gap to + // bridge between adding the htlc to the channel and executing the exit + // hop logic. + ht.AssertInvoiceState(stream, lnrpc.Invoice_ACCEPTED) + + // At this point, Bob decides that he wants to exit the channel + // Alice=>Bob immediately, so he force closes his commitment tx. + closeStream, _ := ht.CloseChannelAssertPending( + bob, aliceChanPoint, true, + ) + + // For anchor channels, the anchor won't be used for CPFP as there's no + // deadline pressure for Bob on the channel Alice->Bob at the moment. + // For Bob's local commitment tx, there's only one incoming HTLC which + // he doesn't have the preimage yet. + hasAnchorSweep := false + bobForceClose := ht.AssertStreamChannelForceClosed( + bob, aliceChanPoint, hasAnchorSweep, closeStream, + ) + + // Alice will offer her anchor output to her sweeper. Her commitment + // output cannot be swept yet as it has incurred an additional CLTV due + // to being the initiator of a script-enforced leased channel. + // + // This anchor output cannot be swept due to it being uneconomical. + ht.AssertNumPendingSweeps(alice, 1) + + // Bob will offer his anchor to his sweeper. + // + // This anchor output cannot be swept due to it being uneconomical. + ht.AssertNumPendingSweeps(bob, 1) + + // Suspend Bob to force Carol to go to chain. + restartBob := ht.SuspendNode(bob) + + // Settle invoice. This will just mark the invoice as settled, as there + // is no link anymore to remove the htlc from the commitment tx. For + // this test, it is important to actually settle and not leave the + // invoice in the accepted state, because without a known preimage, the + // channel arbitrator won't go to chain. + carol.RPC.SettleInvoice(preimage[:]) + + // We now advance the block height to the point where Carol will force + // close her channel with Bob, broadcast the closing tx but keep it + // unconfirmed. + numBlocks := padCLTV( + uint32(invoiceReq.CltvExpiry - incomingBroadcastDelta), + ) + ht.MineBlocks(int(numBlocks) - 1) + + // Expect two txns in the mempool, + // - Carol's force close tx. + // - Carol's CPFP anchor sweeping tx. + // Mine a block to confirm them. + ht.MineBlocksAndAssertNumTxes(1, 2) + + // After the force close tx is mined, Carol should offer her + // second-level success HTLC tx to her sweeper. + ht.AssertNumPendingSweeps(carol, 1) + + // Restart bob again. + require.NoError(ht, restartBob()) + + // Once Bob is online and sees the force close tx Bob=>Carol, he will + // offer his commitment output to his sweeper, which will be skipped + // due to it being timelocked. His anchor outputs will not be swept due + // to uneconomical. We expect to see two sweeping requests, + // - the anchor output from channel Alice=>Bob. + // - the anchor output from channel Bob=>Carol. + ht.AssertNumPendingSweeps(bob, 2) + + // Assert txns can be found in the mempool. + // + // Carol will broadcast her second-level HTLC sweeping txns. Bob canoot + // sweep his commitment anchor output yet due to it being CLTV locked. + ht.AssertNumTxsInMempool(1) + + // At this point we suspend Alice to make sure she'll handle the + // on-chain settle after a restart. + restartAlice := ht.SuspendNode(alice) + + // Mine a block to confirm the sweeping tx from Carol. + ht.MineBlocksAndAssertNumTxes(1, 1) + + // When Bob notices Carol's second level tx in the block, he will + // extract the preimage and broadcast a second level tx to claim the + // HTLC in his (already closed) channel with Alice, which means Bob has + // three sweeping requests, + // - the second level HTLC tx from channel Alice=>Bob. + // - the anchor output from channel Alice=>Bob. + // - the anchor output from channel Bob=>Carol. + ht.AssertNumPendingSweeps(bob, 3) + + // Mine a block to trigger the sweep. This is needed because the + // preimage extraction logic from the link is not managed by the + // blockbeat, which means the preimage may be sent to the contest + // resolver after it's launched. + // + // TODO(yy): Expose blockbeat to the link layer. + ht.MineEmptyBlocks(1) + + // At this point, Bob should have broadcast his second layer success + // tx, and should have sent it to his sweeper. + // + // Check Bob's second level tx. + bobSecondLvlTx := ht.GetNumTxsFromMempool(1)[0] + + // It should spend from the commitment in the channel with Alice. + ht.AssertTxSpendFrom(bobSecondLvlTx, bobForceClose) + + // The channel between Bob and Carol will still be pending force close + // if this is a leased channel. We'd also check the HTLC stages are + // correct in both channels. + ht.AssertNumPendingForceClose(bob, 2) + ht.AssertNumHTLCsAndStage(bob, aliceChanPoint, 1, 1) + ht.AssertNumHTLCsAndStage(bob, bobChanPoint, 1, 1) + + // We'll now mine a block which should confirm Bob's second layer tx. + ht.MineBlocksAndAssertNumTxes(1, 1) + + // Now that the preimage from Bob has hit the chain, restart Alice to + // ensure she'll pick it up. + require.NoError(ht, restartAlice()) + + // If we then mine 1 additional block, Carol's second level tx should + // mature, and she can pull the funds from it with a sweep tx. + resp := ht.AssertNumPendingForceClose(carol, 1)[0] + require.Equal(ht, 1, len(resp.PendingHtlcs)) + + ht.Logf("Carol's timelock to_local output=%v, timelock on second "+ + "stage htlc=%v", resp.BlocksTilMaturity, + resp.PendingHtlcs[0].BlocksTilMaturity) + + ht.MineBlocks(int(resp.PendingHtlcs[0].BlocksTilMaturity)) + + // Carol should have one a sweep request for her second level tx. + ht.AssertNumPendingSweeps(carol, 1) + + // Carol's sweep tx should be broadcast, assert it's in the mempool and + // mine it. + ht.MineBlocksAndAssertNumTxes(1, 1) + + // We now mine blocks till the CSV lock on Bob's success HTLC on + // commitment Alice=>Bob expires. + resp = ht.AssertChannelPendingForceClose(bob, aliceChanPoint) + require.Equal(ht, 1, len(resp.PendingHtlcs)) + htlcExpiry := resp.PendingHtlcs[0].BlocksTilMaturity + + ht.Logf("Bob's timelock to_local output=%v, timelock on second stage "+ + "htlc=%v", resp.BlocksTilMaturity, htlcExpiry) + ht.MineBlocks(int(htlcExpiry)) + + // When we mine one additional block, that will confirm Bob's second + // level HTLC sweep on channel Alice=>Bob. + ht.MineBlocksAndAssertNumTxes(1, 1) + + // We now mine blocks till the CLTV lock on Bob's to_local output HTLC + // on commitment Bob=>Carol expires. + resp = ht.AssertChannelPendingForceClose(bob, bobChanPoint) + require.Equal(ht, 1, len(resp.PendingHtlcs)) + htlcExpiry = resp.PendingHtlcs[0].BlocksTilMaturity + + ht.Logf("Bob's timelock to_local output=%v, timelock on second stage "+ + "htlc=%v", resp.BlocksTilMaturity, htlcExpiry) + ht.MineBlocks(int(resp.BlocksTilMaturity)) + + // Bob should have three requests in his sweeper. + // - to_local output from channel Bob=>Carol. + // - the anchor output from channel Alice=>Bob, uneconomical. + // - the anchor output from channel Bob=>Carol, uneconomical. + ht.AssertNumPendingSweeps(bob, 3) + + // Alice should have two requests in her sweeper, + // - the anchor output from channel Alice=>Bob, uneconomical. + // - her commitment output, now mature. + ht.AssertNumPendingSweeps(alice, 2) + + // Mine a block to confirm Bob's to_local output sweep. + ht.MineBlocksAndAssertNumTxes(1, 2) + + // All nodes should show zero pending and open channels. + for _, node := range []*node.HarnessNode{alice, bob, carol} { + ht.AssertNumPendingForceClose(node, 0) + ht.AssertNodeNumChannels(node, 0) + } + + // Finally, check that the Alice's payment is correctly marked + // succeeded. + ht.AssertPaymentStatus(alice, preimage, lnrpc.Payment_SUCCEEDED) +} diff --git a/itest/lnd_multi-hop_test.go b/itest/lnd_multi-hop_test.go index e6437b053bd..ef50d57a71c 100644 --- a/itest/lnd_multi-hop_test.go +++ b/itest/lnd_multi-hop_test.go @@ -160,367 +160,6 @@ func runMultiHopHtlcClaimTest(ht *lntest.HarnessTest, tester caseRunner) { } } -// testMultiHopHtlcLocalChainClaim tests that in a multi-hop HTLC scenario, if -// we force close a channel with an incoming HTLC, and later find out the -// preimage via the witness beacon, we properly settle the HTLC on-chain using -// the HTLC success transaction in order to ensure we don't lose any funds. -func testMultiHopHtlcLocalChainClaim(ht *lntest.HarnessTest) { - runMultiHopHtlcClaimTest(ht, runMultiHopHtlcLocalChainClaim) -} - -func runMultiHopHtlcLocalChainClaim(ht *lntest.HarnessTest, - alice, bob *node.HarnessNode, c lnrpc.CommitmentType, zeroConf bool) { - - // First, we'll create a three hop network: Alice -> Bob -> Carol, with - // Carol refusing to actually settle or directly cancel any HTLC's - // self. - aliceChanPoint, bobChanPoint, carol := createThreeHopNetwork( - ht, alice, bob, false, c, zeroConf, - ) - - // For neutrino backend, we need to fund one more UTXO for Carol so she - // can sweep her outputs. - if ht.IsNeutrinoBackend() { - ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) - } - - // If this is a taproot channel, then we'll need to make some manual - // route hints so Alice can actually find a route. - var routeHints []*lnrpc.RouteHint - if c == lnrpc.CommitmentType_SIMPLE_TAPROOT { - routeHints = makeRouteHints(bob, carol, zeroConf) - } - - // With the network active, we'll now add a new hodl invoice at Carol's - // end. Make sure the cltv expiry delta is large enough, otherwise Bob - // won't send out the outgoing htlc. - const invoiceAmt = 100000 - var preimage lntypes.Preimage - copy(preimage[:], ht.Random32Bytes()) - payHash := preimage.Hash() - invoiceReq := &invoicesrpc.AddHoldInvoiceRequest{ - Value: invoiceAmt, - CltvExpiry: finalCltvDelta, - Hash: payHash[:], - RouteHints: routeHints, - } - carolInvoice := carol.RPC.AddHoldInvoice(invoiceReq) - - // Subscribe the invoice. - stream := carol.RPC.SubscribeSingleInvoice(payHash[:]) - - // Now that we've created the invoice, we'll send a single payment from - // Alice to Carol. We won't wait for the response however, as Carol - // will not immediately settle the payment. - req := &routerrpc.SendPaymentRequest{ - PaymentRequest: carolInvoice.PaymentRequest, - TimeoutSeconds: 60, - FeeLimitMsat: noFeeLimitMsat, - } - alice.RPC.SendPayment(req) - - // At this point, all 3 nodes should now have an active channel with - // the created HTLC pending on all of them. - ht.AssertActiveHtlcs(alice, payHash[:]) - ht.AssertActiveHtlcs(bob, payHash[:]) - ht.AssertActiveHtlcs(carol, payHash[:]) - - // Wait for carol to mark invoice as accepted. There is a small gap to - // bridge between adding the htlc to the channel and executing the exit - // hop logic. - ht.AssertInvoiceState(stream, lnrpc.Invoice_ACCEPTED) - - // blocksMined records how many blocks have mined after the creation of - // the invoice so it can be used to calculate how many more blocks need - // to be mined to trigger a force close later on. - var blocksMined uint32 - - // At this point, Bob decides that he wants to exit the channel - // immediately, so he force closes his commitment transaction. - closeStream, _ := ht.CloseChannelAssertPending( - bob, aliceChanPoint, true, - ) - - // For anchor channels, the anchor won't be used for CPFP as there's no - // deadline pressure for Bob on the channel Alice->Bob at the moment. - // For Bob's local commitment tx, there's only one incoming HTLC which - // he doesn't have the preimage yet. Thus this anchor won't be - // force-swept. - hasAnchorSweep := false - bobForceClose := ht.AssertStreamChannelForceClosed( - bob, aliceChanPoint, hasAnchorSweep, closeStream, - ) - - // Increase the blocks mined. At this step - // AssertStreamChannelForceClosed mines one block. - blocksMined++ - - var expectedTxes int - switch c { - // Alice will sweep her commitment and anchor output immediately. Bob - // will also offer his anchor to his sweeper. - case lnrpc.CommitmentType_ANCHORS, lnrpc.CommitmentType_SIMPLE_TAPROOT: - ht.AssertNumPendingSweeps(alice, 2) - ht.AssertNumPendingSweeps(bob, 1) - - // We expect to see only one sweeping tx to be published from - // Alice, which sweeps her commit and anchor outputs in the - // same tx. For Bob, since his anchor is not used for CPFP, - // it'd be uneconomical to sweep so it will fail. - expectedTxes = 1 - - // Alice will offer her anchor output to her sweeper. Her commitment - // output cannot be swept yet as it has incurred an additional CLTV due - // to being the initiator of a script-enforced leased channel. - case lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE: - ht.AssertNumPendingSweeps(alice, 1) - ht.AssertNumPendingSweeps(bob, 1) - - // We expect to see only no sweeping txns to be published, - // neither Alice's or Bob's anchor sweep can succeed due to - // it's uneconomical. - expectedTxes = 0 - - default: - ht.Fatalf("unhandled commitment type %v", c) - } - - // Mine a block to trigger the sweeps. - ht.MineEmptyBlocks(1) - blocksMined++ - - // Assert the expected num of txns are found in the mempool. - ht.AssertNumTxsInMempool(expectedTxes) - - // Mine a block to clean up the mempool for the rest of the test. - ht.MineBlocksAndAssertNumTxes(1, expectedTxes) - blocksMined++ - - // Suspend Bob to force Carol to go to chain. - restartBob := ht.SuspendNode(bob) - - // Settle invoice. This will just mark the invoice as settled, as there - // is no link anymore to remove the htlc from the commitment tx. For - // this test, it is important to actually settle and not leave the - // invoice in the accepted state, because without a known preimage, the - // channel arbitrator won't go to chain. - carol.RPC.SettleInvoice(preimage[:]) - - // We now advance the block height to the point where Carol will force - // close her channel with Bob, broadcast the closing tx but keep it - // unconfirmed. - numBlocks := padCLTV(uint32(invoiceReq.CltvExpiry - - lncfg.DefaultIncomingBroadcastDelta)) - ht.MineEmptyBlocks(int(numBlocks - blocksMined)) - - // Carol's commitment transaction should now be in the mempool. - ht.AssertNumTxsInMempool(1) - - // Look up the closing transaction. It should be spending from the - // funding transaction, - closingTx := ht.AssertOutpointInMempool( - ht.OutPointFromChannelPoint(bobChanPoint), - ) - closingTxid := closingTx.TxHash() - - // Mine a block that should confirm the commit tx. - block := ht.MineBlocksAndAssertNumTxes(1, 1)[0] - ht.AssertTxInBlock(block, closingTxid) - - // After the force close transaction is mined, Carol should offer her - // second-level success HTLC tx and anchor to the sweeper. - ht.AssertNumPendingSweeps(carol, 2) - - // Restart bob again. - require.NoError(ht, restartBob()) - - // Lower the fee rate so Bob's two anchor outputs are economical to - // be swept in one tx. - ht.SetFeeEstimate(chainfee.FeePerKwFloor) - - // After the force close transaction is mined, transactions will be - // broadcast by both Bob and Carol. - switch c { - // Carol will broadcast her sweeping txns and Bob will sweep his - // commitment and anchor outputs, we'd expect to see three txns, - // - Carol's second level HTLC transaction. - // - Carol's anchor sweeping txns since it's used for CPFP. - // - Bob's sweep tx spending his commitment output, and two anchor - // outputs, one from channel Alice to Bob and the other from channel - // Bob to Carol. - case lnrpc.CommitmentType_ANCHORS, lnrpc.CommitmentType_SIMPLE_TAPROOT: - ht.AssertNumPendingSweeps(bob, 3) - expectedTxes = 3 - - // Carol will broadcast her sweeping txns and Bob will sweep his - // anchor outputs. Bob can't sweep his commitment output yet as it has - // incurred an additional CLTV due to being the initiator of a - // script-enforced leased channel: - // - Carol's second level HTLC transaction. - // - Carol's anchor sweeping txns since it's used for CPFP. - // - Bob's sweep tx spending his two anchor outputs, one from channel - // Alice to Bob and the other from channel Bob to Carol. - case lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE: - ht.AssertNumPendingSweeps(bob, 2) - expectedTxes = 3 - - default: - ht.Fatalf("unhandled commitment type %v", c) - } - - // Mine a block to trigger the sweeps. - ht.MineEmptyBlocks(1) - - // Assert transactions can be found in the mempool. - ht.AssertNumTxsInMempool(expectedTxes) - - // At this point we suspend Alice to make sure she'll handle the - // on-chain settle after a restart. - restartAlice := ht.SuspendNode(alice) - - // Mine a block to confirm the expected transactions (+ the coinbase). - ht.MineBlocksAndAssertNumTxes(1, expectedTxes) - - // For a channel of the anchor type, we will subtract one block - // from the default CSV, as the Sweeper will handle the input, and the - // Sweeper sweeps the input as soon as the lock expires. - secondLevelMaturity := uint32(defaultCSV - 1) - - // Keep track of the second level tx maturity. - carolSecondLevelCSV := secondLevelMaturity - - // When Bob notices Carol's second level transaction in the block, he - // will extract the preimage and broadcast a second level tx to claim - // the HTLC in his (already closed) channel with Alice. - ht.AssertNumPendingSweeps(bob, 1) - - // Mine a block to trigger the sweep of the second level tx. - ht.MineEmptyBlocks(1) - carolSecondLevelCSV-- - - // Check Bob's second level tx. - bobSecondLvlTx := ht.GetNumTxsFromMempool(1)[0] - - // It should spend from the commitment in the channel with Alice. - ht.AssertTxSpendFrom(bobSecondLvlTx, bobForceClose) - - // At this point, Bob should have broadcast his second layer success - // transaction, and should have sent it to the nursery for incubation. - ht.AssertNumHTLCsAndStage(bob, aliceChanPoint, 1, 1) - - // The channel between Bob and Carol will still be pending force close - // if this is a leased channel. In that case, we'd also check the HTLC - // stages are correct in that channel. - if c == lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE { - ht.AssertNumPendingForceClose(bob, 2) - ht.AssertNumHTLCsAndStage(bob, bobChanPoint, 1, 1) - } else { - ht.AssertNumPendingForceClose(bob, 1) - } - - // We'll now mine a block which should confirm Bob's second layer - // transaction. - ht.MineBlocksAndAssertNumTxes(1, 1) - - // Keep track of Bob's second level maturity, and decrement our track - // of Carol's. - bobSecondLevelCSV := secondLevelMaturity - carolSecondLevelCSV-- - - // Now that the preimage from Bob has hit the chain, restart Alice to - // ensure she'll pick it up. - require.NoError(ht, restartAlice()) - - // If we then mine 1 additional blocks, Carol's second level tx should - // mature, and she can pull the funds from it with a sweep tx. - ht.MineEmptyBlocks(int(carolSecondLevelCSV)) - bobSecondLevelCSV -= carolSecondLevelCSV - - // Carol should have one a sweep request for her second level tx. - ht.AssertNumPendingSweeps(carol, 1) - - // Mine a block to trigger the sweep. - ht.MineEmptyBlocks(1) - bobSecondLevelCSV-- - - // Carol's sweep tx should be broadcast. - carolSweep := ht.AssertNumTxsInMempool(1)[0] - - // Bob should offer his second level tx to his sweeper. - ht.AssertNumPendingSweeps(bob, 1) - - // Mining one additional block, Bob's second level tx is mature, and he - // can sweep the output. - block = ht.MineBlocksAndAssertNumTxes(bobSecondLevelCSV, 1)[0] - ht.AssertTxInBlock(block, carolSweep) - - bobSweep := ht.GetNumTxsFromMempool(1)[0] - bobSweepTxid := bobSweep.TxHash() - - // When we mine one additional block, that will confirm Bob's sweep. - // Now Bob should have no pending channels anymore, as this just - // resolved it by the confirmation of the sweep transaction. - block = ht.MineBlocksAndAssertNumTxes(1, 1)[0] - ht.AssertTxInBlock(block, bobSweepTxid) - - // With the script-enforced lease commitment type, Alice and Bob still - // haven't been able to sweep their respective commit outputs due to the - // additional CLTV. We'll need to mine enough blocks for the timelock to - // expire and prompt their sweep. - if c == lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE { - for _, node := range []*node.HarnessNode{alice, bob} { - ht.AssertNumPendingForceClose(node, 1) - } - - // Due to the way the test is set up, Alice and Bob share the - // same CLTV for their commit outputs even though it's enforced - // on different channels (Alice-Bob and Bob-Carol). - resp := alice.RPC.PendingChannels() - require.Len(ht, resp.PendingForceClosingChannels, 1) - forceCloseChan := resp.PendingForceClosingChannels[0] - require.Positive(ht, forceCloseChan.BlocksTilMaturity) - - // Mine enough blocks for the timelock to expire. - numBlocks := uint32(forceCloseChan.BlocksTilMaturity) - ht.MineEmptyBlocks(int(numBlocks)) - - // Both Alice and Bob should now offer their commit outputs to - // the sweeper. For Alice, she still has her anchor output as - // pending sweep as it's not used for CPFP, thus it's - // uneconomical to sweep it alone. - ht.AssertNumPendingSweeps(alice, 2) - ht.AssertNumPendingSweeps(bob, 1) - - // Mine a block to trigger the sweeps. - ht.MineEmptyBlocks(1) - - // Both Alice and Bob show broadcast their commit sweeps. - aliceCommitOutpoint := wire.OutPoint{ - Hash: bobForceClose, Index: 3, - } - ht.AssertOutpointInMempool( - aliceCommitOutpoint, - ).TxHash() - bobCommitOutpoint := wire.OutPoint{Hash: closingTxid, Index: 3} - ht.AssertOutpointInMempool( - bobCommitOutpoint, - ).TxHash() - - // Confirm their sweeps. - ht.MineBlocksAndAssertNumTxes(1, 2) - } - - // All nodes should show zero pending and open channels. - for _, node := range []*node.HarnessNode{alice, bob, carol} { - ht.AssertNumPendingForceClose(node, 0) - ht.AssertNodeNumChannels(node, 0) - } - - // Finally, check that the Alice's payment is correctly marked - // succeeded. - ht.AssertPaymentStatus(alice, preimage, lnrpc.Payment_SUCCEEDED) -} - // testMultiHopHtlcRemoteChainClaim tests that in the multi-hop HTLC scenario, // if the remote party goes to chain while we have an incoming HTLC, then when // we found out the preimage via the witness beacon, we properly settle the From 52e6fb11615bdb674a666e5e1aa7ef2fcb22c0fa Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Wed, 23 Oct 2024 02:30:44 +0800 Subject: [PATCH 76/92] itest: flatten `testMultiHopHtlcRemoteChainClaim` --- itest/list_on_test.go | 4 - itest/lnd_multi-hop_force_close_test.go | 640 ++++++++++++++++++++++++ itest/lnd_multi-hop_test.go | 295 ----------- 3 files changed, 640 insertions(+), 299 deletions(-) diff --git a/itest/list_on_test.go b/itest/list_on_test.go index f471e777f3d..1edc278c69d 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -297,10 +297,6 @@ var allTestCases = []*lntest.TestCase{ Name: "REST API", TestFunc: testRestAPI, }, - { - Name: "multi hop htlc remote chain claim", - TestFunc: testMultiHopHtlcRemoteChainClaim, - }, { Name: "multi hop htlc aggregation", TestFunc: testMultiHopHtlcAggregation, diff --git a/itest/lnd_multi-hop_force_close_test.go b/itest/lnd_multi-hop_force_close_test.go index 08d2b7d6e7d..120ea35ffa6 100644 --- a/itest/lnd_multi-hop_force_close_test.go +++ b/itest/lnd_multi-hop_force_close_test.go @@ -89,6 +89,18 @@ var multiHopForceCloseTestCases = []*lntest.TestCase{ Name: "multihop local claim incoming htlc leased", TestFunc: testLocalClaimIncomingHTLCLeased, }, + { + Name: "multihop local preimage claim anchor", + TestFunc: testLocalPreimageClaimAnchor, + }, + { + Name: "multihop local preimage claim simple taproot", + TestFunc: testLocalPreimageClaimSimpleTaproot, + }, + { + Name: "multihop local preimage claim leased", + TestFunc: testLocalPreimageClaimLeased, + }, } // testLocalClaimOutgoingHTLCAnchor tests `runLocalClaimOutgoingHTLC` with @@ -255,6 +267,14 @@ func runLocalClaimOutgoingHTLC(ht *lntest.HarnessTest, ht.FundCoins(btcutil.SatoshiPerBitcoin, bob) } + // Bob should have enough wallet UTXOs here to sweep the HTLC in the + // end of this test. However, due to a known issue, Bob's wallet may + // report there's no UTXO available. For details, + // - https://github.com/lightningnetwork/lnd/issues/8786 + // + // TODO(yy): remove this step once the issue is resolved. + ht.FundCoins(btcutil.SatoshiPerBitcoin, bob) + // Now that our channels are set up, we'll send two HTLC's from Alice // to Carol. The first HTLC will be universally considered "dust", // while the second will be a proper fully valued HTLC. @@ -2078,3 +2098,623 @@ func runLocalClaimIncomingHTLCLeased(ht *lntest.HarnessTest, // succeeded. ht.AssertPaymentStatus(alice, preimage, lnrpc.Payment_SUCCEEDED) } + +// testLocalPreimageClaimAnchor tests `runLocalPreimageClaim` with anchor +// channel. +func testLocalPreimageClaimAnchor(ht *lntest.HarnessTest) { + success := ht.Run("no zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // anchor channels. + // + // Prepare params. + params := lntest.OpenChannelParams{Amt: chanAmt} + + cfg := node.CfgAnchor + cfgs := [][]string{cfg, cfg, cfg} + + runLocalPreimageClaim(st, cfgs, params) + }) + if !success { + return + } + + ht.Run("zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // zero-conf anchor channels. + // + // Prepare params. + params := lntest.OpenChannelParams{ + Amt: chanAmt, + ZeroConf: true, + CommitmentType: lnrpc.CommitmentType_ANCHORS, + } + + // Prepare Carol's node config to enable zero-conf and anchor. + cfg := node.CfgZeroConf + cfgs := [][]string{cfg, cfg, cfg} + + runLocalPreimageClaim(st, cfgs, params) + }) +} + +// testLocalPreimageClaimSimpleTaproot tests `runLocalClaimIncomingHTLC` with +// simple taproot channel. +func testLocalPreimageClaimSimpleTaproot(ht *lntest.HarnessTest) { + c := lnrpc.CommitmentType_SIMPLE_TAPROOT + + success := ht.Run("no zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // simple taproot channels. + // + // Prepare params. + params := lntest.OpenChannelParams{ + Amt: chanAmt, + CommitmentType: c, + Private: true, + } + + cfg := node.CfgSimpleTaproot + cfgs := [][]string{cfg, cfg, cfg} + + runLocalPreimageClaim(st, cfgs, params) + }) + if !success { + return + } + + ht.Run("zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // zero-conf simple taproot channels. + // + // Prepare params. + params := lntest.OpenChannelParams{ + Amt: chanAmt, + ZeroConf: true, + CommitmentType: c, + Private: true, + } + + // Prepare Carol's node config to enable zero-conf and leased + // channel. + cfg := node.CfgSimpleTaproot + cfg = append(cfg, node.CfgZeroConf...) + cfgs := [][]string{cfg, cfg, cfg} + + runLocalPreimageClaim(st, cfgs, params) + }) +} + +// runLocalPreimageClaim tests that in the multi-hop HTLC scenario, if the +// remote party goes to chain while we have an incoming HTLC, then when we +// found out the preimage via the witness beacon, we properly settle the HTLC +// directly on-chain using the preimage in order to ensure that we don't lose +// any funds. +func runLocalPreimageClaim(ht *lntest.HarnessTest, + cfgs [][]string, params lntest.OpenChannelParams) { + + // Set the min relay feerate to be 10 sat/vbyte so the non-CPFP anchor + // is never swept. + // + // TODO(yy): delete this line once the normal anchor sweeping is + // removed. + ht.SetMinRelayFeerate(10_000) + + // Create a three hop network: Alice -> Bob -> Carol. + chanPoints, nodes := ht.CreateSimpleNetwork(cfgs, params) + alice, bob, carol := nodes[0], nodes[1], nodes[2] + aliceChanPoint := chanPoints[0] + + // Fund Carol one UTXO so she can sweep outputs. + ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) + + // Carol should have enough wallet UTXOs here to sweep the HTLC in the + // end of this test. However, due to a known issue, Carol's wallet may + // report there's no UTXO available. For details, + // - https://github.com/lightningnetwork/lnd/issues/8786 + // + // TODO(yy): remove this step once the issue is resolved. + ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) + + // If this is a taproot channel, then we'll need to make some manual + // route hints so Alice can actually find a route. + var routeHints []*lnrpc.RouteHint + if params.CommitmentType == lnrpc.CommitmentType_SIMPLE_TAPROOT { + routeHints = makeRouteHints(bob, carol, params.ZeroConf) + } + + // With the network active, we'll now add a new hodl invoice at Carol's + // end. Make sure the cltv expiry delta is large enough, otherwise Bob + // won't send out the outgoing htlc. + preimage := ht.RandomPreimage() + payHash := preimage.Hash() + + invoiceReq := &invoicesrpc.AddHoldInvoiceRequest{ + Value: invoiceAmt, + CltvExpiry: finalCltvDelta, + Hash: payHash[:], + RouteHints: routeHints, + } + carolInvoice := carol.RPC.AddHoldInvoice(invoiceReq) + + // Subscribe the invoice. + stream := carol.RPC.SubscribeSingleInvoice(payHash[:]) + + // Now that we've created the invoice, we'll send a single payment from + // Alice to Carol. We won't wait for the response however, as Carol + // will not immediately settle the payment. + req := &routerrpc.SendPaymentRequest{ + PaymentRequest: carolInvoice.PaymentRequest, + TimeoutSeconds: 60, + FeeLimitMsat: noFeeLimitMsat, + } + alice.RPC.SendPayment(req) + + // At this point, all 3 nodes should now have an active channel with + // the created HTLC pending on all of them. + ht.AssertActiveHtlcs(alice, payHash[:]) + ht.AssertActiveHtlcs(bob, payHash[:]) + ht.AssertActiveHtlcs(carol, payHash[:]) + + // Wait for carol to mark invoice as accepted. There is a small gap to + // bridge between adding the htlc to the channel and executing the exit + // hop logic. + ht.AssertInvoiceState(stream, lnrpc.Invoice_ACCEPTED) + + // Record the height which the invoice will expire. + invoiceExpiry := ht.CurrentHeight() + uint32(invoiceReq.CltvExpiry) + + // Next, Alice decides that she wants to exit the channel, so she'll + // immediately force close the channel by broadcast her commitment + // transaction. + closeStream, _ := ht.CloseChannelAssertPending( + alice, aliceChanPoint, true, + ) + aliceForceClose := ht.AssertStreamChannelForceClosed( + alice, aliceChanPoint, true, closeStream, + ) + + // Wait for the channel to be marked pending force close. + ht.AssertChannelPendingForceClose(alice, aliceChanPoint) + + // Once the force closing tx is mined, Alice should offer the anchor + // output to her sweeper. + ht.AssertNumPendingSweeps(alice, 1) + + // Bob should offer his anchor output to his sweeper. + ht.AssertNumPendingSweeps(bob, 1) + + // Mine enough blocks for Alice to sweep her funds from the force + // closed channel. AssertStreamChannelForceClosed() already mined a + // block, so mine one less than defaultCSV in order to perform mempool + // assertions. + ht.MineBlocks(defaultCSV - 1) + + // Mine Alice's commit sweeping tx. + ht.MineBlocksAndAssertNumTxes(1, 1) + + // Suspend bob, so Carol is forced to go on chain. + restartBob := ht.SuspendNode(bob) + + // Settle invoice. This will just mark the invoice as settled, as there + // is no link anymore to remove the htlc from the commitment tx. For + // this test, it is important to actually settle and not leave the + // invoice in the accepted state, because without a known preimage, the + // channel arbitrator won't go to chain. + carol.RPC.SettleInvoice(preimage[:]) + + ht.Logf("Invoice expire height: %d, current: %d", invoiceExpiry, + ht.CurrentHeight()) + + // We'll now mine enough blocks so Carol decides that she needs to go + // on-chain to claim the HTLC as Bob has been inactive. + numBlocks := padCLTV( + invoiceExpiry - ht.CurrentHeight() - incomingBroadcastDelta, + ) + ht.MineBlocks(int(numBlocks)) + + // Since Carol has time-sensitive HTLCs, she will use the anchor for + // CPFP purpose. Assert the anchor output is offered to the sweeper. + // + // For neutrino backend, Carol still have the two anchors - one from + // local commitment and the other from the remote. + if ht.IsNeutrinoBackend() { + ht.AssertNumPendingSweeps(carol, 2) + } else { + ht.AssertNumPendingSweeps(carol, 1) + } + + // We should see two txns in the mempool, we now a block to confirm, + // - Carol's force close tx. + // - Carol's anchor sweeping tx. + ht.MineBlocksAndAssertNumTxes(1, 2) + + // Once the force close tx is confirmed, Carol should offer her + // incoming HTLC to her sweeper. + ht.AssertNumPendingSweeps(carol, 1) + + // Restart bob again. + require.NoError(ht, restartBob()) + + // Bob should have three sweeping requests, + // - the anchor output from channel Alice=>Bob, uneconomical. + // - the anchor output from channel Bob=>Carol, uneconomical. + // - the commit output sweep from the channel with Carol, no timelock. + ht.AssertNumPendingSweeps(bob, 3) + + // Mine an empty block the for neutrino backend. We need this step to + // trigger Bob's chain watcher to detect the force close tx. Deep down, + // this happens because the notification system for neutrino is very + // different from others. Specifically, when a block contains the force + // close tx is notified, these two calls, + // - RegisterBlockEpochNtfn, will notify the block first. + // - RegisterSpendNtfn, will wait for the neutrino notifier to sync to + // the block, then perform a GetUtxo, which, by the time the spend + // details are sent, the blockbeat is considered processed in Bob's + // chain watcher. + // + // TODO(yy): refactor txNotifier to fix the above issue. + if ht.IsNeutrinoBackend() { + ht.MineEmptyBlocks(1) + } + + // We mine one block to confirm, + // - Carol's sweeping tx of the incoming HTLC. + // - Bob's sweeping tx of his commit output. + ht.MineBlocksAndAssertNumTxes(1, 2) + + // When Bob notices Carol's second level tx in the block, he will + // extract the preimage and offer the HTLC to his sweeper. So he has, + // - the anchor output from channel Alice=>Bob, uneconomical. + // - the anchor output from channel Bob=>Carol, uneconomical. + // - the htlc sweeping tx. + ht.AssertNumPendingSweeps(bob, 3) + + // Mine an empty block the for neutrino backend. We need this step to + // trigger Bob's chain watcher to detect the force close tx. Deep down, + // this happens because the notification system for neutrino is very + // different from others. Specifically, when a block contains the force + // close tx is notified, these two calls, + // - RegisterBlockEpochNtfn, will notify the block first. + // - RegisterSpendNtfn, will wait for the neutrino notifier to sync to + // the block, then perform a GetUtxo, which, by the time the spend + // details are sent, the blockbeat is considered processed in Bob's + // chain watcher. + // + // TODO(yy): refactor txNotifier to fix the above issue. + if ht.IsNeutrinoBackend() { + ht.MineEmptyBlocks(1) + } + + // Mine a block to trigger the sweep. This is needed because the + // preimage extraction logic from the link is not managed by the + // blockbeat, which means the preimage may be sent to the contest + // resolver after it's launched. + // + // TODO(yy): Expose blockbeat to the link layer. + ht.MineEmptyBlocks(1) + + // Bob should broadcast the sweeping of the direct preimage spent now. + bobHtlcSweep := ht.GetNumTxsFromMempool(1)[0] + + // It should spend from the commitment in the channel with Alice. + ht.AssertTxSpendFrom(bobHtlcSweep, aliceForceClose) + + // We'll now mine a block which should confirm Bob's HTLC sweep tx. + ht.MineBlocksAndAssertNumTxes(1, 1) + + // Now that the sweeping tx has been confirmed, Bob should recognize + // that all contracts for the Bob-Carol channel have been fully + // resolved. + ht.AssertNumPendingForceClose(bob, 0) + + // Mine blocks till Carol's second level tx matures. + resp := ht.AssertNumPendingForceClose(carol, 1)[0] + require.Equal(ht, 1, len(resp.PendingHtlcs)) + + ht.Logf("Carol's timelock to_local output=%v, timelock on second "+ + "stage htlc=%v", resp.BlocksTilMaturity, + resp.PendingHtlcs[0].BlocksTilMaturity) + + ht.MineBlocks(int(resp.PendingHtlcs[0].BlocksTilMaturity)) + + // Carol should offer the htlc output to her sweeper. + ht.AssertNumPendingSweeps(carol, 1) + + // Mine a block to confirm Carol's sweeping tx. + ht.MineBlocksAndAssertNumTxes(1, 1) + + // When Carol's sweep gets confirmed, she should have no more pending + // channels. + ht.AssertNumPendingForceClose(carol, 0) + + // The invoice should show as settled for Carol, indicating that it was + // swept on-chain. + ht.AssertInvoiceState(stream, lnrpc.Invoice_SETTLED) + + // Finally, check that the Alice's payment is correctly marked + // succeeded. + ht.AssertPaymentStatus(alice, preimage, lnrpc.Payment_SUCCEEDED) +} + +// testLocalPreimageClaimLeased tests `runLocalPreimageClaim` with script +// enforced lease channel. +func testLocalPreimageClaimLeased(ht *lntest.HarnessTest) { + success := ht.Run("no zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // leased channels. + // + // Prepare params. + params := lntest.OpenChannelParams{ + Amt: chanAmt, + CommitmentType: leasedType, + } + + cfg := node.CfgLeased + cfgs := [][]string{cfg, cfg, cfg} + + runLocalPreimageClaimLeased(st, cfgs, params) + }) + if !success { + return + } + + ht.Run("zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // zero-conf anchor channels. + // + // Prepare params. + params := lntest.OpenChannelParams{ + Amt: chanAmt, + ZeroConf: true, + CommitmentType: leasedType, + } + + // Prepare Carol's node config to enable zero-conf and leased + // channel. + cfg := node.CfgLeased + cfg = append(cfg, node.CfgZeroConf...) + cfgs := [][]string{cfg, cfg, cfg} + + runLocalPreimageClaimLeased(st, cfgs, params) + }) +} + +// runLocalPreimageClaimLeased tests that in the multi-hop HTLC scenario, if +// the remote party goes to chain while we have an incoming HTLC, then when we +// found out the preimage via the witness beacon, we properly settle the HTLC +// directly on-chain using the preimage in order to ensure that we don't lose +// any funds. +func runLocalPreimageClaimLeased(ht *lntest.HarnessTest, + cfgs [][]string, params lntest.OpenChannelParams) { + + // Set the min relay feerate to be 10 sat/vbyte so the non-CPFP anchor + // is never swept. + // + // TODO(yy): delete this line once the normal anchor sweeping is + // removed. + ht.SetMinRelayFeerate(10_000) + + // Create a three hop network: Alice -> Bob -> Carol. + chanPoints, nodes := ht.CreateSimpleNetwork(cfgs, params) + alice, bob, carol := nodes[0], nodes[1], nodes[2] + aliceChanPoint, bobChanPoint := chanPoints[0], chanPoints[1] + + // Fund Carol one UTXO so she can sweep outputs. + ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) + + // With the network active, we'll now add a new hodl invoice at Carol's + // end. Make sure the cltv expiry delta is large enough, otherwise Bob + // won't send out the outgoing htlc. + preimage := ht.RandomPreimage() + payHash := preimage.Hash() + + invoiceReq := &invoicesrpc.AddHoldInvoiceRequest{ + Value: invoiceAmt, + CltvExpiry: finalCltvDelta, + Hash: payHash[:], + } + carolInvoice := carol.RPC.AddHoldInvoice(invoiceReq) + + // Subscribe the invoice. + stream := carol.RPC.SubscribeSingleInvoice(payHash[:]) + + // Now that we've created the invoice, we'll send a single payment from + // Alice to Carol. We won't wait for the response however, as Carol + // will not immediately settle the payment. + req := &routerrpc.SendPaymentRequest{ + PaymentRequest: carolInvoice.PaymentRequest, + TimeoutSeconds: 60, + FeeLimitMsat: noFeeLimitMsat, + } + alice.RPC.SendPayment(req) + + // At this point, all 3 nodes should now have an active channel with + // the created HTLC pending on all of them. + ht.AssertActiveHtlcs(alice, payHash[:]) + ht.AssertActiveHtlcs(bob, payHash[:]) + ht.AssertActiveHtlcs(carol, payHash[:]) + + // Wait for carol to mark invoice as accepted. There is a small gap to + // bridge between adding the htlc to the channel and executing the exit + // hop logic. + ht.AssertInvoiceState(stream, lnrpc.Invoice_ACCEPTED) + + // Record the height which the invoice will expire. + invoiceExpiry := ht.CurrentHeight() + uint32(invoiceReq.CltvExpiry) + + // Next, Alice decides that she wants to exit the channel, so she'll + // immediately force close the channel by broadcast her commitment + // transaction. + closeStream, _ := ht.CloseChannelAssertPending( + alice, aliceChanPoint, true, + ) + aliceForceClose := ht.AssertStreamChannelForceClosed( + alice, aliceChanPoint, true, closeStream, + ) + + // Wait for the channel to be marked pending force close. + ht.AssertChannelPendingForceClose(alice, aliceChanPoint) + + // Once the force closing tx is mined, Alice should offer the anchor + // output to her sweeper. + ht.AssertNumPendingSweeps(alice, 1) + + // Bob should offer his anchor output to his sweeper. + ht.AssertNumPendingSweeps(bob, 1) + + // Suspend bob, so Carol is forced to go on chain. + restartBob := ht.SuspendNode(bob) + + // Settle invoice. This will just mark the invoice as settled, as there + // is no link anymore to remove the htlc from the commitment tx. For + // this test, it is important to actually settle and not leave the + // invoice in the accepted state, because without a known preimage, the + // channel arbitrator won't go to chain. + carol.RPC.SettleInvoice(preimage[:]) + + ht.Logf("Invoice expire height: %d, current: %d", invoiceExpiry, + ht.CurrentHeight()) + + // We'll now mine enough blocks so Carol decides that she needs to go + // on-chain to claim the HTLC as Bob has been inactive. + numBlocks := padCLTV( + invoiceExpiry - ht.CurrentHeight() - incomingBroadcastDelta - 1, + ) + ht.MineBlocks(int(numBlocks)) + + // Since Carol has time-sensitive HTLCs, she will use the anchor for + // CPFP purpose. Assert the anchor output is offered to the sweeper. + // + // For neutrino backend, there's no way to know the sweeping of the + // remote anchor is failed, so Carol still sees two pending sweeps. + if ht.IsNeutrinoBackend() { + ht.AssertNumPendingSweeps(carol, 2) + } else { + ht.AssertNumPendingSweeps(carol, 1) + } + + // We should see two txns in the mempool, we now a block to confirm, + // - Carol's force close tx. + // - Carol's anchor sweeping tx. + ht.MineBlocksAndAssertNumTxes(1, 2) + + // Once the force close tx is confirmed, Carol should offer her + // incoming HTLC to her sweeper. + ht.AssertNumPendingSweeps(carol, 1) + + // Restart bob again. + require.NoError(ht, restartBob()) + + // Bob should have two sweeping requests, + // - the anchor output from channel Alice=>Bob, uneconomical. + // - the anchor output from channel Bob=>Carol, uneconomical. + // - the commit output sweep from the channel with Carol, which is CLTV + // locked so it won't show up the pending sweeps. + ht.AssertNumPendingSweeps(bob, 2) + + // We mine one block to confirm, + // - Carol's sweeping tx of the incoming HTLC. + ht.MineBlocksAndAssertNumTxes(1, 1) + + // When Bob notices Carol's second level tx in the block, he will + // extract the preimage and offer the HTLC to his sweeper. So he has, + // - the anchor output from channel Alice=>Bob, uneconomical. + // - the anchor output from channel Bob=>Carol, uneconomical. + // - the htlc sweeping tx. + ht.AssertNumPendingSweeps(bob, 3) + + // Mine a block to trigger the sweep. This is needed because the + // preimage extraction logic from the link is not managed by the + // blockbeat, which means the preimage may be sent to the contest + // resolver after it's launched. + // + // TODO(yy): Expose blockbeat to the link layer. + ht.MineEmptyBlocks(1) + + // Bob should broadcast the sweeping of the direct preimage spent now. + bobHtlcSweep := ht.GetNumTxsFromMempool(1)[0] + + // It should spend from the commitment in the channel with Alice. + ht.AssertTxSpendFrom(bobHtlcSweep, aliceForceClose) + + // We'll now mine a block which should confirm Bob's HTLC sweep tx. + ht.MineBlocksAndAssertNumTxes(1, 1) + + // Now that the sweeping tx has been confirmed, Bob should recognize + // that all contracts for the Bob-Carol channel have been fully + // resolved. + ht.AssertNumPendingForceClose(bob, 1) + ht.AssertChannelPendingForceClose(bob, bobChanPoint) + + // Mine blocks till Carol's second level tx matures. + resp := ht.AssertNumPendingForceClose(carol, 1)[0] + require.Equal(ht, 1, len(resp.PendingHtlcs)) + + ht.Logf("Carol's timelock to_local output=%v, timelock on second "+ + "stage htlc=%v", resp.BlocksTilMaturity, + resp.PendingHtlcs[0].BlocksTilMaturity) + + ht.MineBlocks(int(resp.PendingHtlcs[0].BlocksTilMaturity)) + + // Carol should offer the htlc output to her sweeper. + ht.AssertNumPendingSweeps(carol, 1) + + // Mine a block to confirm Carol's sweeping tx. + ht.MineBlocksAndAssertNumTxes(1, 1) + + // When Carol's sweep gets confirmed, she should have no more pending + // channels. + ht.AssertNumPendingForceClose(carol, 0) + + // The invoice should show as settled for Carol, indicating that it was + // swept on-chain. + ht.AssertInvoiceState(stream, lnrpc.Invoice_SETTLED) + + // Check that the Alice's payment is correctly marked succeeded. + ht.AssertPaymentStatus(alice, preimage, lnrpc.Payment_SUCCEEDED) + + // With the script-enforced lease commitment type, Alice and Bob still + // haven't been able to sweep their respective commit outputs due to + // the additional CLTV. We'll need to mine enough blocks for the + // timelock to expire and prompt their sweep. + // + // Get num of blocks to mine. + resp = ht.AssertNumPendingForceClose(alice, 1)[0] + require.Equal(ht, 1, len(resp.PendingHtlcs)) + + ht.Logf("Alice's timelock to_local output=%v, timelock on second "+ + "stage htlc=%v", resp.BlocksTilMaturity, + resp.PendingHtlcs[0].BlocksTilMaturity) + + ht.MineBlocks(int(resp.BlocksTilMaturity)) + + // Alice should two sweeping requests, + // - the anchor output from channel Alice=>Bob, uneconomical. + // - the commit output sweep from the channel with Bob. + ht.AssertNumPendingSweeps(alice, 2) + + // Bob should have three sweeping requests, + // - the anchor output from channel Alice=>Bob, uneconomical. + // - the anchor output from channel Bob=>Carol, uneconomical. + // - the commit output sweep from the channel with Carol. + ht.AssertNumPendingSweeps(bob, 3) + + // Confirm their sweeps. + ht.MineBlocksAndAssertNumTxes(1, 2) + + // Both nodes should consider the channel fully closed. + ht.AssertNumPendingForceClose(alice, 0) + ht.AssertNumPendingForceClose(bob, 0) +} diff --git a/itest/lnd_multi-hop_test.go b/itest/lnd_multi-hop_test.go index ef50d57a71c..33f91f383a2 100644 --- a/itest/lnd_multi-hop_test.go +++ b/itest/lnd_multi-hop_test.go @@ -18,7 +18,6 @@ import ( "github.com/lightningnetwork/lnd/lntest/rpc" "github.com/lightningnetwork/lnd/lntest/wait" "github.com/lightningnetwork/lnd/lntypes" - "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/routing" "github.com/stretchr/testify/require" ) @@ -160,300 +159,6 @@ func runMultiHopHtlcClaimTest(ht *lntest.HarnessTest, tester caseRunner) { } } -// testMultiHopHtlcRemoteChainClaim tests that in the multi-hop HTLC scenario, -// if the remote party goes to chain while we have an incoming HTLC, then when -// we found out the preimage via the witness beacon, we properly settle the -// HTLC directly on-chain using the preimage in order to ensure that we don't -// lose any funds. -func testMultiHopHtlcRemoteChainClaim(ht *lntest.HarnessTest) { - runMultiHopHtlcClaimTest(ht, runMultiHopHtlcRemoteChainClaim) -} - -func runMultiHopHtlcRemoteChainClaim(ht *lntest.HarnessTest, - alice, bob *node.HarnessNode, c lnrpc.CommitmentType, zeroConf bool) { - - // First, we'll create a three hop network: Alice -> Bob -> Carol, with - // Carol refusing to actually settle or directly cancel any HTLC's - // self. - aliceChanPoint, bobChanPoint, carol := createThreeHopNetwork( - ht, alice, bob, false, c, zeroConf, - ) - - // If this is a taproot channel, then we'll need to make some manual - // route hints so Alice can actually find a route. - var routeHints []*lnrpc.RouteHint - if c == lnrpc.CommitmentType_SIMPLE_TAPROOT { - routeHints = makeRouteHints(bob, carol, zeroConf) - } - - // With the network active, we'll now add a new hodl invoice at Carol's - // end. Make sure the cltv expiry delta is large enough, otherwise Bob - // won't send out the outgoing htlc. - const invoiceAmt = 100000 - var preimage lntypes.Preimage - copy(preimage[:], ht.Random32Bytes()) - payHash := preimage.Hash() - invoiceReq := &invoicesrpc.AddHoldInvoiceRequest{ - Value: invoiceAmt, - CltvExpiry: finalCltvDelta, - Hash: payHash[:], - RouteHints: routeHints, - } - carolInvoice := carol.RPC.AddHoldInvoice(invoiceReq) - - // Subscribe the invoice. - stream := carol.RPC.SubscribeSingleInvoice(payHash[:]) - - // Now that we've created the invoice, we'll send a single payment from - // Alice to Carol. We won't wait for the response however, as Carol - // will not immediately settle the payment. - req := &routerrpc.SendPaymentRequest{ - PaymentRequest: carolInvoice.PaymentRequest, - TimeoutSeconds: 60, - FeeLimitMsat: noFeeLimitMsat, - } - alice.RPC.SendPayment(req) - - // At this point, all 3 nodes should now have an active channel with - // the created HTLC pending on all of them. - ht.AssertActiveHtlcs(alice, payHash[:]) - ht.AssertActiveHtlcs(bob, payHash[:]) - ht.AssertActiveHtlcs(carol, payHash[:]) - - // Wait for carol to mark invoice as accepted. There is a small gap to - // bridge between adding the htlc to the channel and executing the exit - // hop logic. - ht.AssertInvoiceState(stream, lnrpc.Invoice_ACCEPTED) - - // blocksMined records how many blocks have mined after the creation of - // the invoice so it can be used to calculate how many more blocks need - // to be mined to trigger a force close later on. - var blocksMined int - - // Lower the fee rate so Bob's two anchor outputs are economical to - // be swept in one tx. - ht.SetFeeEstimate(chainfee.FeePerKwFloor) - - // Next, Alice decides that she wants to exit the channel, so she'll - // immediately force close the channel by broadcast her commitment - // transaction. - closeStream, _ := ht.CloseChannelAssertPending( - alice, aliceChanPoint, true, - ) - aliceForceClose := ht.AssertStreamChannelForceClosed( - alice, aliceChanPoint, true, closeStream, - ) - - // Increase the blocks mined. At this step - // AssertStreamChannelForceClosed mines one block. - blocksMined++ - - // Wait for the channel to be marked pending force close. - ht.AssertChannelPendingForceClose(alice, aliceChanPoint) - - // After AssertStreamChannelForceClosed returns, it has mined a block - // so now bob will attempt to redeem his anchor output. Check the - // anchor is offered to the sweeper. - ht.AssertNumPendingSweeps(bob, 1) - ht.AssertNumPendingSweeps(alice, 1) - - // Mine enough blocks for Alice to sweep her funds from the force - // closed channel. AssertStreamChannelForceClosed() already mined a - // block containing the commitment tx and the commit sweep tx will be - // broadcast immediately before it can be included in a block, so mine - // one less than defaultCSV in order to perform mempool assertions. - if c != lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE { - ht.MineEmptyBlocks(defaultCSV - blocksMined) - blocksMined = defaultCSV - - // Alice should now sweep her funds. - ht.AssertNumPendingSweeps(alice, 2) - - // Mine a block to trigger the sweep. - ht.MineEmptyBlocks(1) - blocksMined++ - - // Mine Alice's commit sweeping tx. - ht.MineBlocksAndAssertNumTxes(1, 1) - blocksMined++ - } - - // Suspend bob, so Carol is forced to go on chain. - restartBob := ht.SuspendNode(bob) - - // Settle invoice. This will just mark the invoice as settled, as there - // is no link anymore to remove the htlc from the commitment tx. For - // this test, it is important to actually settle and not leave the - // invoice in the accepted state, because without a known preimage, the - // channel arbitrator won't go to chain. - carol.RPC.SettleInvoice(preimage[:]) - - // We'll now mine enough blocks so Carol decides that she needs to go - // on-chain to claim the HTLC as Bob has been inactive. - numBlocks := padCLTV(uint32( - invoiceReq.CltvExpiry - lncfg.DefaultIncomingBroadcastDelta, - )) - ht.MineEmptyBlocks(int(numBlocks) - blocksMined) - - // Carol's commitment transaction should now be in the mempool. - ht.AssertNumTxsInMempool(1) - - // The closing transaction should be spending from the funding - // transaction. - closingTx := ht.AssertOutpointInMempool( - ht.OutPointFromChannelPoint(bobChanPoint), - ) - closingTxid := closingTx.TxHash() - - // Since Carol has time-sensitive HTLCs, she will use the anchor for - // CPFP purpose. Assert she has two pending anchor sweep requests - one - // from local commit and the other from remote commit. - ht.AssertNumPendingSweeps(carol, 2) - - // Mine a block, which should contain: the commitment. - block := ht.MineBlocksAndAssertNumTxes(1, 1)[0] - ht.AssertTxInBlock(block, closingTxid) - - // After the force close transaction is mined, Carol should offer her - // second level HTLC tx to the sweeper, along with her anchor output. - ht.AssertNumPendingSweeps(carol, 2) - - // Restart bob again. - require.NoError(ht, restartBob()) - - // After the force close transaction is mined, we should expect Bob and - // Carol to broadcast some transactions depending on the channel - // commitment type. - switch c { - // Carol should broadcast her second level HTLC transaction and Bob - // should broadcast a sweeping tx to sweep his commitment output and - // anchor outputs from the two channels. - case lnrpc.CommitmentType_ANCHORS, lnrpc.CommitmentType_SIMPLE_TAPROOT: - ht.AssertNumPendingSweeps(bob, 3) - - // Carol should broadcast her second level HTLC transaction and Bob - // should broadcast a transaction to sweep his anchor outputs. Bob - // can't sweep his commitment output yet as he has incurred an - // additional CLTV due to being the channel initiator of a force closed - // script-enforced leased channel. - case lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE: - ht.AssertNumPendingSweeps(bob, 2) - - default: - ht.Fatalf("unhandled commitment type %v", c) - } - - // Keep track of the second level tx maturity. - carolSecondLevelCSV := uint32(defaultCSV) - - // Mine a block to trigger the sweeps, also confirms Carol's CPFP - // anchor sweeping. - ht.MineBlocksAndAssertNumTxes(1, 1) - carolSecondLevelCSV-- - ht.AssertNumTxsInMempool(2) - - // Mine a block to confirm the expected transactions. - ht.MineBlocksAndAssertNumTxes(1, 2) - - // When Bob notices Carol's second level transaction in the block, he - // will extract the preimage and offer the HTLC to his sweeper. - ht.AssertNumPendingSweeps(bob, 1) - - // NOTE: after Bob is restarted, the sweeping of the direct preimage - // spent will happen immediately so we don't need to mine a block to - // trigger Bob's sweeper to sweep it. - bobHtlcSweep := ht.GetNumTxsFromMempool(1)[0] - bobHtlcSweepTxid := bobHtlcSweep.TxHash() - - // It should spend from the commitment in the channel with Alice. - ht.AssertTxSpendFrom(bobHtlcSweep, aliceForceClose) - - // We'll now mine a block which should confirm Bob's HTLC sweep - // transaction. - block = ht.MineBlocksAndAssertNumTxes(1, 1)[0] - ht.AssertTxInBlock(block, bobHtlcSweepTxid) - carolSecondLevelCSV-- - - // Now that the sweeping transaction has been confirmed, Bob should now - // recognize that all contracts for the Bob-Carol channel have been - // fully resolved - aliceBobPendingChansLeft := 0 - if c == lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE { - aliceBobPendingChansLeft = 1 - } - for _, node := range []*node.HarnessNode{alice, bob} { - ht.AssertNumPendingForceClose( - node, aliceBobPendingChansLeft, - ) - } - - // If we then mine 3 additional blocks, Carol's second level tx will - // mature, and she should pull the funds. - ht.MineEmptyBlocks(int(carolSecondLevelCSV)) - ht.AssertNumPendingSweeps(carol, 1) - - // Mine a block to trigger the sweep of the second level tx. - ht.MineEmptyBlocks(1) - carolSweep := ht.AssertNumTxsInMempool(1)[0] - - // When Carol's sweep gets confirmed, she should have no more pending - // channels. - block = ht.MineBlocksAndAssertNumTxes(1, 1)[0] - ht.AssertTxInBlock(block, carolSweep) - ht.AssertNumPendingForceClose(carol, 0) - - // With the script-enforced lease commitment type, Alice and Bob still - // haven't been able to sweep their respective commit outputs due to the - // additional CLTV. We'll need to mine enough blocks for the timelock to - // expire and prompt their sweep. - if c == lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE { - // Due to the way the test is set up, Alice and Bob share the - // same CLTV for their commit outputs even though it's enforced - // on different channels (Alice-Bob and Bob-Carol). - resp := alice.RPC.PendingChannels() - require.Len(ht, resp.PendingForceClosingChannels, 1) - forceCloseChan := resp.PendingForceClosingChannels[0] - require.Positive(ht, forceCloseChan.BlocksTilMaturity) - - // Mine enough blocks for the timelock to expire. - numBlocks := int(forceCloseChan.BlocksTilMaturity) - ht.MineEmptyBlocks(numBlocks) - - // Both Alice and Bob should offer their commit sweeps. - ht.AssertNumPendingSweeps(alice, 2) - ht.AssertNumPendingSweeps(bob, 1) - - // Mine a block to trigger the sweeps. - ht.MineEmptyBlocks(1) - - // Both Alice and Bob should broadcast their commit sweeps. - aliceCommitOutpoint := wire.OutPoint{ - Hash: aliceForceClose, Index: 3, - } - ht.AssertOutpointInMempool(aliceCommitOutpoint) - bobCommitOutpoint := wire.OutPoint{Hash: closingTxid, Index: 3} - ht.AssertOutpointInMempool(bobCommitOutpoint) - - // Confirm their sweeps. - ht.MineBlocksAndAssertNumTxes(1, 2) - - // Alice and Bob should not show any pending channels anymore as - // they have been fully resolved. - for _, node := range []*node.HarnessNode{alice, bob} { - ht.AssertNumPendingForceClose(node, 0) - } - } - - // The invoice should show as settled for Carol, indicating that it was - // swept on-chain. - invoice := ht.AssertInvoiceState(stream, lnrpc.Invoice_SETTLED) - require.Equal(ht, int64(invoiceAmt), invoice.AmtPaidSat) - - // Finally, check that the Alice's payment is correctly marked - // succeeded. - ht.AssertPaymentStatus(alice, preimage, lnrpc.Payment_SUCCEEDED) -} - // testMultiHopHtlcAggregation tests that in a multi-hop HTLC scenario, if we // force close a channel with both incoming and outgoing HTLCs, we can properly // resolve them using the second level timeout and success transactions. In From f95e64f084326498a12f85e567d17c6471e6b3a6 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Wed, 23 Oct 2024 22:51:48 +0800 Subject: [PATCH 77/92] itest: flatten `testMultiHopHtlcAggregation` --- itest/list_on_test.go | 4 - itest/lnd_multi-hop_force_close_test.go | 423 ++++++++++++++++++++++ itest/lnd_multi-hop_test.go | 457 ------------------------ 3 files changed, 423 insertions(+), 461 deletions(-) diff --git a/itest/list_on_test.go b/itest/list_on_test.go index 1edc278c69d..ac440ff5335 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -297,10 +297,6 @@ var allTestCases = []*lntest.TestCase{ Name: "REST API", TestFunc: testRestAPI, }, - { - Name: "multi hop htlc aggregation", - TestFunc: testMultiHopHtlcAggregation, - }, { Name: "revoked uncooperative close retribution", TestFunc: testRevokedCloseRetribution, diff --git a/itest/lnd_multi-hop_force_close_test.go b/itest/lnd_multi-hop_force_close_test.go index 120ea35ffa6..46631bbda93 100644 --- a/itest/lnd_multi-hop_force_close_test.go +++ b/itest/lnd_multi-hop_force_close_test.go @@ -10,6 +10,7 @@ import ( "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/lntest" "github.com/lightningnetwork/lnd/lntest/node" + "github.com/lightningnetwork/lnd/lntest/rpc" "github.com/lightningnetwork/lnd/lntypes" "github.com/stretchr/testify/require" ) @@ -101,6 +102,18 @@ var multiHopForceCloseTestCases = []*lntest.TestCase{ Name: "multihop local preimage claim leased", TestFunc: testLocalPreimageClaimLeased, }, + { + Name: "multihop htlc aggregation anchor", + TestFunc: testHtlcAggregaitonAnchor, + }, + { + Name: "multihop htlc aggregation simple taproot", + TestFunc: testHtlcAggregaitonSimpleTaproot, + }, + { + Name: "multihop htlc aggregation leased", + TestFunc: testHtlcAggregaitonLeased, + }, } // testLocalClaimOutgoingHTLCAnchor tests `runLocalClaimOutgoingHTLC` with @@ -2718,3 +2731,413 @@ func runLocalPreimageClaimLeased(ht *lntest.HarnessTest, ht.AssertNumPendingForceClose(alice, 0) ht.AssertNumPendingForceClose(bob, 0) } + +// testHtlcAggregaitonAnchor tests `runHtlcAggregation` with anchor channel. +func testHtlcAggregaitonAnchor(ht *lntest.HarnessTest) { + success := ht.Run("no zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // anchor channels. + // + // Prepare params. + params := lntest.OpenChannelParams{Amt: chanAmt} + + cfg := node.CfgAnchor + cfgs := [][]string{cfg, cfg, cfg} + + runHtlcAggregation(st, cfgs, params) + }) + if !success { + return + } + + ht.Run("zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // zero-conf anchor channels. + // + // Prepare params. + params := lntest.OpenChannelParams{ + Amt: chanAmt, + ZeroConf: true, + CommitmentType: lnrpc.CommitmentType_ANCHORS, + } + + // Prepare Carol's node config to enable zero-conf and anchor. + cfg := node.CfgZeroConf + cfgs := [][]string{cfg, cfg, cfg} + + runHtlcAggregation(st, cfgs, params) + }) +} + +// testHtlcAggregaitonSimpleTaproot tests `runHtlcAggregation` with simple +// taproot channel. +func testHtlcAggregaitonSimpleTaproot(ht *lntest.HarnessTest) { + c := lnrpc.CommitmentType_SIMPLE_TAPROOT + + success := ht.Run("no zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // simple taproot channels. + // + // Prepare params. + params := lntest.OpenChannelParams{ + Amt: chanAmt, + CommitmentType: c, + Private: true, + } + + cfg := node.CfgSimpleTaproot + cfgs := [][]string{cfg, cfg, cfg} + + runHtlcAggregation(st, cfgs, params) + }) + if !success { + return + } + + ht.Run("zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // zero-conf simple taproot channels. + // + // Prepare params. + params := lntest.OpenChannelParams{ + Amt: chanAmt, + ZeroConf: true, + CommitmentType: c, + Private: true, + } + + // Prepare Carol's node config to enable zero-conf and leased + // channel. + cfg := node.CfgSimpleTaproot + cfg = append(cfg, node.CfgZeroConf...) + cfgs := [][]string{cfg, cfg, cfg} + + runHtlcAggregation(st, cfgs, params) + }) +} + +// testHtlcAggregaitonLeased tests `runHtlcAggregation` with script enforced +// lease channel. +func testHtlcAggregaitonLeased(ht *lntest.HarnessTest) { + success := ht.Run("no zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // leased channels. + // + // Prepare params. + params := lntest.OpenChannelParams{ + Amt: chanAmt, + CommitmentType: leasedType, + } + + cfg := node.CfgLeased + cfgs := [][]string{cfg, cfg, cfg} + + runHtlcAggregation(st, cfgs, params) + }) + if !success { + return + } + + ht.Run("zero conf", func(t *testing.T) { + st := ht.Subtest(t) + + // Create a three hop network: Alice -> Bob -> Carol, using + // zero-conf anchor channels. + // + // Prepare params. + params := lntest.OpenChannelParams{ + Amt: chanAmt, + ZeroConf: true, + CommitmentType: leasedType, + } + + // Prepare Carol's node config to enable zero-conf and leased + // channel. + cfg := node.CfgLeased + cfg = append(cfg, node.CfgZeroConf...) + cfgs := [][]string{cfg, cfg, cfg} + + runHtlcAggregation(st, cfgs, params) + }) +} + +// runHtlcAggregation tests that in a multi-hop HTLC scenario, if we force +// close a channel with both incoming and outgoing HTLCs, we can properly +// resolve them using the second level timeout and success transactions. In +// case of anchor channels, the second-level spends can also be aggregated and +// properly feebumped, so we'll check that as well. +func runHtlcAggregation(ht *lntest.HarnessTest, + cfgs [][]string, params lntest.OpenChannelParams) { + + // Set the min relay feerate to be 10 sat/vbyte so the non-CPFP anchor + // is never swept. + // + // TODO(yy): delete this line once the normal anchor sweeping is + // removed. + ht.SetMinRelayFeerate(10_000) + + // Create a three hop network: Alice -> Bob -> Carol. + chanPoints, nodes := ht.CreateSimpleNetwork(cfgs, params) + alice, bob, carol := nodes[0], nodes[1], nodes[2] + _, bobChanPoint := chanPoints[0], chanPoints[1] + + // We need one additional UTXO to create the sweeping tx for the + // second-level success txes. + ht.FundCoins(btcutil.SatoshiPerBitcoin, bob) + + // Bob should have enough wallet UTXOs here to sweep the HTLC in the + // end of this test. However, due to a known issue, Bob's wallet may + // report there's no UTXO available. For details, + // - https://github.com/lightningnetwork/lnd/issues/8786 + // + // TODO(yy): remove this step once the issue is resolved. + ht.FundCoins(btcutil.SatoshiPerBitcoin, bob) + + // If this is a taproot channel, then we'll need to make some manual + // route hints so Alice+Carol can actually find a route. + var ( + carolRouteHints []*lnrpc.RouteHint + aliceRouteHints []*lnrpc.RouteHint + ) + + if params.CommitmentType == lnrpc.CommitmentType_SIMPLE_TAPROOT { + carolRouteHints = makeRouteHints(bob, carol, params.ZeroConf) + aliceRouteHints = makeRouteHints(bob, alice, params.ZeroConf) + } + + // To ensure we have capacity in both directions of the route, we'll + // make a fairly large payment Alice->Carol and settle it. + const reBalanceAmt = 500_000 + invoice := &lnrpc.Invoice{ + Value: reBalanceAmt, + RouteHints: carolRouteHints, + } + invResp := carol.RPC.AddInvoice(invoice) + ht.CompletePaymentRequests(alice, []string{invResp.PaymentRequest}) + + // Make sure Carol has settled the invoice. + ht.AssertInvoiceSettled(carol, invResp.PaymentAddr) + + // With the network active, we'll now add a new hodl invoices at both + // Alice's and Carol's end. Make sure the cltv expiry delta is large + // enough, otherwise Bob won't send out the outgoing htlc. + const numInvoices = 5 + const invoiceAmt = 50_000 + + var ( + carolInvoices []*invoicesrpc.AddHoldInvoiceResp + aliceInvoices []*invoicesrpc.AddHoldInvoiceResp + alicePreimages []lntypes.Preimage + payHashes [][]byte + invoiceStreamsCarol []rpc.SingleInvoiceClient + invoiceStreamsAlice []rpc.SingleInvoiceClient + ) + + // Add Carol invoices. + for i := 0; i < numInvoices; i++ { + preimage := ht.RandomPreimage() + payHash := preimage.Hash() + + invoiceReq := &invoicesrpc.AddHoldInvoiceRequest{ + Value: invoiceAmt, + CltvExpiry: finalCltvDelta, + Hash: payHash[:], + RouteHints: carolRouteHints, + } + carolInvoice := carol.RPC.AddHoldInvoice(invoiceReq) + + carolInvoices = append(carolInvoices, carolInvoice) + payHashes = append(payHashes, payHash[:]) + + // Subscribe the invoice. + stream := carol.RPC.SubscribeSingleInvoice(payHash[:]) + invoiceStreamsCarol = append(invoiceStreamsCarol, stream) + } + + // We'll give Alice's invoices a longer CLTV expiry, to ensure the + // channel Bob<->Carol will be closed first. + for i := 0; i < numInvoices; i++ { + preimage := ht.RandomPreimage() + payHash := preimage.Hash() + + invoiceReq := &invoicesrpc.AddHoldInvoiceRequest{ + Value: invoiceAmt, + CltvExpiry: thawHeightDelta - 4, + Hash: payHash[:], + RouteHints: aliceRouteHints, + } + aliceInvoice := alice.RPC.AddHoldInvoice(invoiceReq) + + aliceInvoices = append(aliceInvoices, aliceInvoice) + alicePreimages = append(alicePreimages, preimage) + payHashes = append(payHashes, payHash[:]) + + // Subscribe the invoice. + stream := alice.RPC.SubscribeSingleInvoice(payHash[:]) + invoiceStreamsAlice = append(invoiceStreamsAlice, stream) + } + + // Now that we've created the invoices, we'll pay them all from + // Alice<->Carol, going through Bob. We won't wait for the response + // however, as neither will immediately settle the payment. + // + // Alice will pay all of Carol's invoices. + for _, carolInvoice := range carolInvoices { + req := &routerrpc.SendPaymentRequest{ + PaymentRequest: carolInvoice.PaymentRequest, + TimeoutSeconds: 60, + FeeLimitMsat: noFeeLimitMsat, + } + alice.RPC.SendPayment(req) + } + + // And Carol will pay Alice's. + for _, aliceInvoice := range aliceInvoices { + req := &routerrpc.SendPaymentRequest{ + PaymentRequest: aliceInvoice.PaymentRequest, + TimeoutSeconds: 60, + FeeLimitMsat: noFeeLimitMsat, + } + carol.RPC.SendPayment(req) + } + + // At this point, all 3 nodes should now the HTLCs active on their + // channels. + ht.AssertActiveHtlcs(alice, payHashes...) + ht.AssertActiveHtlcs(bob, payHashes...) + ht.AssertActiveHtlcs(carol, payHashes...) + + // Wait for Alice and Carol to mark the invoices as accepted. There is + // a small gap to bridge between adding the htlc to the channel and + // executing the exit hop logic. + for _, stream := range invoiceStreamsCarol { + ht.AssertInvoiceState(stream, lnrpc.Invoice_ACCEPTED) + } + + for _, stream := range invoiceStreamsAlice { + ht.AssertInvoiceState(stream, lnrpc.Invoice_ACCEPTED) + } + + // We want Carol's htlcs to expire off-chain to demonstrate bob's force + // close. However, Carol will cancel her invoices to prevent force + // closes, so we shut her down for now. + restartCarol := ht.SuspendNode(carol) + + // We'll now mine enough blocks to trigger Bob's broadcast of his + // commitment transaction due to the fact that the Carol's HTLCs are + // about to timeout. With the default outgoing broadcast delta of zero, + // this will be the same height as the htlc expiry height. + numBlocks := padCLTV( + uint32(finalCltvDelta - lncfg.DefaultOutgoingBroadcastDelta), + ) + ht.MineBlocks(int(numBlocks)) + + // Bob should have one anchor sweep request. + // + // For neutrino backend, there's no way to know the sweeping of the + // remote anchor is failed, so Bob still sees two pending sweeps. + if ht.IsNeutrinoBackend() { + ht.AssertNumPendingSweeps(bob, 2) + } else { + ht.AssertNumPendingSweeps(bob, 1) + } + + // Bob's force close tx and anchor sweeping tx should now be found in + // the mempool. + ht.AssertNumTxsInMempool(2) + + // Mine a block to confirm Bob's force close tx and anchor sweeping tx. + ht.MineBlocksAndAssertNumTxes(1, 2) + + // Bob should have `numInvoices` for HTLC timeout txns. + ht.AssertNumPendingSweeps(bob, numInvoices) + + // Once bob has force closed, we can restart carol. + require.NoError(ht, restartCarol()) + + // Carol should have commit and anchor outputs. + ht.AssertNumPendingSweeps(carol, 2) + + // Let Alice settle her invoices. When Bob now gets the preimages, he + // will broadcast his second-level txns to claim the htlcs. + for _, preimage := range alicePreimages { + alice.RPC.SettleInvoice(preimage[:]) + } + + // Bob should have `numInvoices` for both HTLC success and timeout + // txns. + ht.AssertNumPendingSweeps(bob, numInvoices*2) + + // Mine a block to trigger the sweep. This is needed because the + // preimage extraction logic from the link is not managed by the + // blockbeat, which means the preimage may be sent to the contest + // resolver after it's launched. + // + // TODO(yy): Expose blockbeat to the link layer. + ht.MineEmptyBlocks(1) + + // We expect to see three sweeping txns: + // 1. Bob's sweeping tx for all timeout HTLCs. + // 2. Bob's sweeping tx for all success HTLCs. + // 3. Carol's sweeping tx for her commit output. + // Mine a block to confirm them. + ht.MineBlocksAndAssertNumTxes(1, 3) + + // For this channel, we also check the number of HTLCs and the stage + // are correct. + ht.AssertNumHTLCsAndStage(bob, bobChanPoint, numInvoices*2, 2) + + // For non-leased channels, we can now mine one block so Bob will sweep + // his to_local output. + if params.CommitmentType != leasedType { + // Mine one block so Bob's to_local becomes mature. + ht.MineBlocks(1) + + // Bob should offer the to_local output to his sweeper now. + ht.AssertNumPendingSweeps(bob, 1) + + // Mine a block to confirm Bob's sweeping of his to_local + // output. + ht.MineBlocksAndAssertNumTxes(1, 1) + } + + // Mine blocks till the CSV expires on Bob's HTLC output. + resp := ht.AssertNumPendingForceClose(bob, 1)[0] + require.Equal(ht, numInvoices*2, len(resp.PendingHtlcs)) + + ht.Logf("Bob's timelock to_local output=%v, timelock on second stage "+ + "htlc=%v", resp.BlocksTilMaturity, + resp.PendingHtlcs[0].BlocksTilMaturity) + + ht.MineBlocks(int(resp.PendingHtlcs[0].BlocksTilMaturity)) + + // With the above mined block, Bob's HTLCs should now all be offered to + // his sweeper since the CSV lock is now expired. + // + // For leased channel, due to the test setup, Bob's to_local output is + // now also mature and can be swept together with his HTLCs. + if params.CommitmentType == leasedType { + ht.AssertNumPendingSweeps(bob, numInvoices*2+1) + } else { + ht.AssertNumPendingSweeps(bob, numInvoices*2) + } + + // When we mine one additional block, that will confirm Bob's second + // level sweep. Now Bob should have no pending channels anymore, as + // this just resolved it by the confirmation of the sweep tx. + ht.MineBlocksAndAssertNumTxes(1, 1) + ht.AssertNumPendingForceClose(bob, 0) + + // Carol should have no channels left. + ht.AssertNumPendingForceClose(carol, 0) +} diff --git a/itest/lnd_multi-hop_test.go b/itest/lnd_multi-hop_test.go index 33f91f383a2..124118af6ab 100644 --- a/itest/lnd_multi-hop_test.go +++ b/itest/lnd_multi-hop_test.go @@ -6,7 +6,6 @@ import ( "testing" "github.com/btcsuite/btcd/btcutil" - "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/chainreg" "github.com/lightningnetwork/lnd/lncfg" @@ -16,8 +15,6 @@ import ( "github.com/lightningnetwork/lnd/lntest" "github.com/lightningnetwork/lnd/lntest/node" "github.com/lightningnetwork/lnd/lntest/rpc" - "github.com/lightningnetwork/lnd/lntest/wait" - "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/routing" "github.com/stretchr/testify/require" ) @@ -159,460 +156,6 @@ func runMultiHopHtlcClaimTest(ht *lntest.HarnessTest, tester caseRunner) { } } -// testMultiHopHtlcAggregation tests that in a multi-hop HTLC scenario, if we -// force close a channel with both incoming and outgoing HTLCs, we can properly -// resolve them using the second level timeout and success transactions. In -// case of anchor channels, the second-level spends can also be aggregated and -// properly feebumped, so we'll check that as well. -func testMultiHopHtlcAggregation(ht *lntest.HarnessTest) { - runMultiHopHtlcClaimTest(ht, runMultiHopHtlcAggregation) -} - -func runMultiHopHtlcAggregation(ht *lntest.HarnessTest, - alice, bob *node.HarnessNode, c lnrpc.CommitmentType, zeroConf bool) { - - // We need one additional UTXO to create the sweeping tx for the - // second-level success txes. - ht.FundCoins(btcutil.SatoshiPerBitcoin, bob) - - // First, we'll create a three hop network: Alice -> Bob -> Carol. - aliceChanPoint, bobChanPoint, carol := createThreeHopNetwork( - ht, alice, bob, false, c, zeroConf, - ) - - // If this is a taproot channel, then we'll need to make some manual - // route hints so Alice+Carol can actually find a route. - var ( - carolRouteHints []*lnrpc.RouteHint - aliceRouteHints []*lnrpc.RouteHint - ) - if c == lnrpc.CommitmentType_SIMPLE_TAPROOT { - carolRouteHints = makeRouteHints(bob, carol, zeroConf) - aliceRouteHints = makeRouteHints(bob, alice, zeroConf) - } - - // To ensure we have capacity in both directions of the route, we'll - // make a fairly large payment Alice->Carol and settle it. - const reBalanceAmt = 500_000 - invoice := &lnrpc.Invoice{ - Value: reBalanceAmt, - RouteHints: carolRouteHints, - } - resp := carol.RPC.AddInvoice(invoice) - ht.CompletePaymentRequests(alice, []string{resp.PaymentRequest}) - - // Make sure Carol has settled the invoice. - ht.AssertInvoiceSettled(carol, resp.PaymentAddr) - - // With the network active, we'll now add a new hodl invoices at both - // Alice's and Carol's end. Make sure the cltv expiry delta is large - // enough, otherwise Bob won't send out the outgoing htlc. - const numInvoices = 5 - const invoiceAmt = 50_000 - - var ( - carolInvoices []*invoicesrpc.AddHoldInvoiceResp - aliceInvoices []*invoicesrpc.AddHoldInvoiceResp - alicePreimages []lntypes.Preimage - payHashes [][]byte - invoiceStreamsCarol []rpc.SingleInvoiceClient - invoiceStreamsAlice []rpc.SingleInvoiceClient - ) - - // Add Carol invoices. - for i := 0; i < numInvoices; i++ { - var preimage lntypes.Preimage - copy(preimage[:], ht.Random32Bytes()) - payHash := preimage.Hash() - invoiceReq := &invoicesrpc.AddHoldInvoiceRequest{ - Value: invoiceAmt, - CltvExpiry: finalCltvDelta, - Hash: payHash[:], - RouteHints: carolRouteHints, - } - carolInvoice := carol.RPC.AddHoldInvoice(invoiceReq) - - carolInvoices = append(carolInvoices, carolInvoice) - payHashes = append(payHashes, payHash[:]) - - // Subscribe the invoice. - stream := carol.RPC.SubscribeSingleInvoice(payHash[:]) - invoiceStreamsCarol = append(invoiceStreamsCarol, stream) - } - - // We'll give Alice's invoices a longer CLTV expiry, to ensure the - // channel Bob<->Carol will be closed first. - for i := 0; i < numInvoices; i++ { - var preimage lntypes.Preimage - copy(preimage[:], ht.Random32Bytes()) - payHash := preimage.Hash() - invoiceReq := &invoicesrpc.AddHoldInvoiceRequest{ - Value: invoiceAmt, - CltvExpiry: thawHeightDelta - 4, - Hash: payHash[:], - RouteHints: aliceRouteHints, - } - aliceInvoice := alice.RPC.AddHoldInvoice(invoiceReq) - - aliceInvoices = append(aliceInvoices, aliceInvoice) - alicePreimages = append(alicePreimages, preimage) - payHashes = append(payHashes, payHash[:]) - - // Subscribe the invoice. - stream := alice.RPC.SubscribeSingleInvoice(payHash[:]) - invoiceStreamsAlice = append(invoiceStreamsAlice, stream) - } - - // Now that we've created the invoices, we'll pay them all from - // Alice<->Carol, going through Bob. We won't wait for the response - // however, as neither will immediately settle the payment. - - // Alice will pay all of Carol's invoices. - for _, carolInvoice := range carolInvoices { - req := &routerrpc.SendPaymentRequest{ - PaymentRequest: carolInvoice.PaymentRequest, - TimeoutSeconds: 60, - FeeLimitMsat: noFeeLimitMsat, - } - alice.RPC.SendPayment(req) - } - - // And Carol will pay Alice's. - for _, aliceInvoice := range aliceInvoices { - req := &routerrpc.SendPaymentRequest{ - PaymentRequest: aliceInvoice.PaymentRequest, - TimeoutSeconds: 60, - FeeLimitMsat: noFeeLimitMsat, - } - carol.RPC.SendPayment(req) - } - - // At this point, all 3 nodes should now the HTLCs active on their - // channels. - ht.AssertActiveHtlcs(alice, payHashes...) - ht.AssertActiveHtlcs(bob, payHashes...) - ht.AssertActiveHtlcs(carol, payHashes...) - - // Wait for Alice and Carol to mark the invoices as accepted. There is - // a small gap to bridge between adding the htlc to the channel and - // executing the exit hop logic. - for _, stream := range invoiceStreamsCarol { - ht.AssertInvoiceState(stream, lnrpc.Invoice_ACCEPTED) - } - - for _, stream := range invoiceStreamsAlice { - ht.AssertInvoiceState(stream, lnrpc.Invoice_ACCEPTED) - } - - // Increase the fee estimate so that the following force close tx will - // be cpfp'ed. - ht.SetFeeEstimate(30000) - - // We want Carol's htlcs to expire off-chain to demonstrate bob's force - // close. However, Carol will cancel her invoices to prevent force - // closes, so we shut her down for now. - restartCarol := ht.SuspendNode(carol) - - // We'll now mine enough blocks to trigger Bob's broadcast of his - // commitment transaction due to the fact that the Carol's HTLCs are - // about to timeout. With the default outgoing broadcast delta of zero, - // this will be the same height as the htlc expiry height. - numBlocks := padCLTV( - uint32(finalCltvDelta - lncfg.DefaultOutgoingBroadcastDelta), - ) - ht.MineEmptyBlocks(int(numBlocks)) - - // Bob's force close transaction should now be found in the mempool. If - // there are anchors, we expect it to be offered to Bob's sweeper. - ht.AssertNumTxsInMempool(1) - - // Bob has two anchor sweep requests, one for remote (invalid) and the - // other for local. - ht.AssertNumPendingSweeps(bob, 2) - - closeTx := ht.AssertOutpointInMempool( - ht.OutPointFromChannelPoint(bobChanPoint), - ) - closeTxid := closeTx.TxHash() - - // Go through the closing transaction outputs, and make an index for - // the HTLC outputs. - successOuts := make(map[wire.OutPoint]struct{}) - timeoutOuts := make(map[wire.OutPoint]struct{}) - for i, txOut := range closeTx.TxOut { - op := wire.OutPoint{ - Hash: closeTxid, - Index: uint32(i), - } - - switch txOut.Value { - // If this HTLC goes towards Carol, Bob will claim it with a - // timeout Tx. In this case the value will be the invoice - // amount. - case invoiceAmt: - timeoutOuts[op] = struct{}{} - - // If the HTLC has direction towards Alice, Bob will claim it - // with the success TX when he learns the preimage. In this - // case one extra sat will be on the output, because of the - // routing fee. - case invoiceAmt + 1: - successOuts[op] = struct{}{} - } - } - - // Once bob has force closed, we can restart carol. - require.NoError(ht, restartCarol()) - - // Mine a block to confirm the closing transaction. - ht.MineBlocksAndAssertNumTxes(1, 1) - - // The above mined block will trigger Bob to sweep his anchor output. - ht.AssertNumTxsInMempool(1) - - // Let Alice settle her invoices. When Bob now gets the preimages, he - // has no other option than to broadcast his second-level transactions - // to claim the money. - for _, preimage := range alicePreimages { - alice.RPC.SettleInvoice(preimage[:]) - } - - expectedTxes := 0 - switch c { - // In case of anchors, all success transactions will be aggregated into - // one, the same is the case for the timeout transactions. In this case - // Carol will also sweep her commitment and anchor output in a single - // tx. - case lnrpc.CommitmentType_ANCHORS, - lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE, - lnrpc.CommitmentType_SIMPLE_TAPROOT: - - // Bob should have `numInvoices` for both HTLC success and - // timeout txns, plus one anchor sweep. - ht.AssertNumPendingSweeps(bob, numInvoices*2+1) - - // Carol should have commit and anchor outputs. - ht.AssertNumPendingSweeps(carol, 2) - - // We expect to see three sweeping txns: - // 1. Bob's sweeping tx for all timeout HTLCs. - // 2. Bob's sweeping tx for all success HTLCs. - // 3. Carol's sweeping tx for her commit and anchor outputs. - expectedTxes = 3 - - default: - ht.Fatalf("unhandled commitment type %v", c) - } - - // Mine a block to confirm Bob's anchor sweeping, which will also - // trigger his sweeper to sweep HTLCs. - ht.MineBlocksAndAssertNumTxes(1, 1) - - // Assert the sweeping txns are found in the mempool. - txes := ht.GetNumTxsFromMempool(expectedTxes) - - // Since Bob can aggregate the transactions, we expect a single - // transaction, that have multiple spends from the commitment. - var ( - timeoutTxs []*chainhash.Hash - successTxs []*chainhash.Hash - ) - for _, tx := range txes { - txid := tx.TxHash() - - for i := range tx.TxIn { - prevOp := tx.TxIn[i].PreviousOutPoint - if _, ok := successOuts[prevOp]; ok { - successTxs = append(successTxs, &txid) - - break - } - - if _, ok := timeoutOuts[prevOp]; ok { - timeoutTxs = append(timeoutTxs, &txid) - - break - } - } - } - - // In case of anchor we expect all the timeout and success second - // levels to be aggregated into one tx. For earlier channel types, they - // will be separate transactions. - if lntest.CommitTypeHasAnchors(c) { - require.Len(ht, timeoutTxs, 1) - require.Len(ht, successTxs, 1) - } else { - require.Len(ht, timeoutTxs, numInvoices) - require.Len(ht, successTxs, numInvoices) - } - - // All mempool transactions should be spending from the commitment - // transaction. - ht.AssertAllTxesSpendFrom(txes, closeTxid) - - // Mine a block to confirm the all the transactions, including Carol's - // commitment tx, anchor tx(optional), and Bob's second-level timeout - // and success txes. - ht.MineBlocksAndAssertNumTxes(1, expectedTxes) - - // At this point, Bob should have broadcast his second layer success - // transaction, and should have sent it to the nursery for incubation, - // or to the sweeper for sweeping. - forceCloseChan := ht.AssertNumPendingForceClose(bob, 1)[0] - ht.Logf("Bob's timelock on commit=%v, timelock on htlc=%v", - forceCloseChan.BlocksTilMaturity, - forceCloseChan.PendingHtlcs[0].BlocksTilMaturity) - - // For this channel, we also check the number of HTLCs and the stage - // are correct. - ht.AssertNumHTLCsAndStage(bob, bobChanPoint, numInvoices*2, 2) - - if c != lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE { - // If we then mine additional blocks, Bob can sweep his - // commitment output. - ht.MineEmptyBlocks(1) - - // Assert the tx has been offered to the sweeper. - ht.AssertNumPendingSweeps(bob, 1) - - // Mine one block to trigger the sweep. - ht.MineEmptyBlocks(1) - - // Find the commitment sweep. - bobCommitSweep := ht.GetNumTxsFromMempool(1)[0] - ht.AssertTxSpendFrom(bobCommitSweep, closeTxid) - - // Also ensure it is not spending from any of the HTLC output. - for _, txin := range bobCommitSweep.TxIn { - for _, timeoutTx := range timeoutTxs { - require.NotEqual(ht, *timeoutTx, - txin.PreviousOutPoint.Hash, - "found unexpected spend of timeout tx") - } - - for _, successTx := range successTxs { - require.NotEqual(ht, *successTx, - txin.PreviousOutPoint.Hash, - "found unexpected spend of success tx") - } - } - } - - switch c { - // Mining one additional block, Bob's second level tx is mature, and he - // can sweep the output. Before the blocks are mined, we should expect - // to see Bob's commit sweep in the mempool. - case lnrpc.CommitmentType_ANCHORS, lnrpc.CommitmentType_SIMPLE_TAPROOT: - ht.MineBlocksAndAssertNumTxes(1, 1) - - // Since Bob is the initiator of the Bob-Carol script-enforced leased - // channel, he incurs an additional CLTV when sweeping outputs back to - // his wallet. We'll need to mine enough blocks for the timelock to - // expire to prompt his broadcast. - case lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE: - resp := bob.RPC.PendingChannels() - require.Len(ht, resp.PendingForceClosingChannels, 1) - forceCloseChan := resp.PendingForceClosingChannels[0] - require.Positive(ht, forceCloseChan.BlocksTilMaturity) - numBlocks := uint32(forceCloseChan.BlocksTilMaturity) - - // Add debug log. - height := ht.CurrentHeight() - bob.AddToLogf("itest: now mine %d blocks at height %d", - numBlocks, height) - ht.MineEmptyBlocks(int(numBlocks) - 1) - - default: - ht.Fatalf("unhandled commitment type %v", c) - } - - // Make sure Bob's sweeper has received all the sweeping requests. - ht.AssertNumPendingSweeps(bob, numInvoices*2) - - // Mine one block to trigger the sweeps. - ht.MineEmptyBlocks(1) - - // For leased channels, Bob's commit output will mature after the above - // block. - if c == lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE { - ht.AssertNumPendingSweeps(bob, numInvoices*2+1) - } - - // We now wait for 30 seconds to overcome the flake - there's a block - // race between contractcourt and sweeper, causing the sweep to be - // broadcast earlier. - // - // TODO(yy): remove this once `blockbeat` is in place. - numExpected := 1 - err := wait.NoError(func() error { - mem := ht.GetRawMempool() - if len(mem) == numExpected { - return nil - } - - if len(mem) > 0 { - numExpected = len(mem) - } - - return fmt.Errorf("want %d, got %v in mempool: %v", numExpected, - len(mem), mem) - }, wait.DefaultTimeout) - ht.Logf("Checking mempool got: %v", err) - - // Make sure it spends from the second level tx. - secondLevelSweep := ht.GetNumTxsFromMempool(numExpected)[0] - bobSweep := secondLevelSweep.TxHash() - - // It should be sweeping all the second-level outputs. - var secondLvlSpends int - for _, txin := range secondLevelSweep.TxIn { - for _, timeoutTx := range timeoutTxs { - if *timeoutTx == txin.PreviousOutPoint.Hash { - secondLvlSpends++ - } - } - - for _, successTx := range successTxs { - if *successTx == txin.PreviousOutPoint.Hash { - secondLvlSpends++ - } - } - } - - // TODO(yy): bring the following check back when `blockbeat` is in - // place - atm we may have two sweeping transactions in the mempool. - // require.Equal(ht, 2*numInvoices, secondLvlSpends) - - // When we mine one additional block, that will confirm Bob's second - // level sweep. Now Bob should have no pending channels anymore, as - // this just resolved it by the confirmation of the sweep transaction. - block := ht.MineBlocksAndAssertNumTxes(1, numExpected)[0] - ht.AssertTxInBlock(block, bobSweep) - - // For leased channels, we need to mine one more block to confirm Bob's - // commit output sweep. - // - // NOTE: we mine this block conditionally, as the commit output may - // have already been swept one block earlier due to the race in block - // consumption among subsystems. - pendingChanResp := bob.RPC.PendingChannels() - if len(pendingChanResp.PendingForceClosingChannels) != 0 { - ht.MineBlocksAndAssertNumTxes(1, 1) - } - ht.AssertNumPendingForceClose(bob, 0) - - // THe channel with Alice is still open. - ht.AssertNodeNumChannels(bob, 1) - - // Carol should have no channels left (open nor pending). - ht.AssertNumPendingForceClose(carol, 0) - ht.AssertNodeNumChannels(carol, 0) - - // Coop close, no anchors. - ht.CloseChannel(alice, aliceChanPoint) -} - // createThreeHopNetwork creates a topology of `Alice -> Bob -> Carol`. func createThreeHopNetwork(ht *lntest.HarnessTest, alice, bob *node.HarnessNode, carolHodl bool, c lnrpc.CommitmentType, From 34951a61534c5f86185f8f159bbf6a44c2377c61 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Wed, 23 Oct 2024 23:57:19 +0800 Subject: [PATCH 78/92] itest: flatten `testHtlcTimeoutResolverExtractPreimageLocal` This commit simplfies the test since we only test the preimage extraction logic in the htlc timeout resolver, there's no need to test it for all different channel types as the resolver is made to be oblivious about them. --- itest/lnd_multi-hop_test.go | 175 ++++++++++++++---------------------- 1 file changed, 69 insertions(+), 106 deletions(-) diff --git a/itest/lnd_multi-hop_test.go b/itest/lnd_multi-hop_test.go index 124118af6ab..ff37dae22b1 100644 --- a/itest/lnd_multi-hop_test.go +++ b/itest/lnd_multi-hop_test.go @@ -6,7 +6,6 @@ import ( "testing" "github.com/btcsuite/btcd/btcutil" - "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/chainreg" "github.com/lightningnetwork/lnd/lncfg" "github.com/lightningnetwork/lnd/lnrpc" @@ -478,31 +477,38 @@ func runExtraPreimageFromRemoteCommit(ht *lntest.HarnessTest, // testHtlcTimeoutResolverExtractPreimage tests that in the multi-hop setting, // Alice->Bob->Carol, when Bob's outgoing HTLC is swept by Carol using the // direct preimage spend, Bob's timeout resolver will extract the preimage from -// the sweep tx found in mempool or blocks(for neutrino). The direct spend tx -// is broadcast by Carol and spends the outpoint on Bob's commit tx. +// the sweep tx found in mempool. The direct spend tx is broadcast by Carol and +// spends the outpoint on Bob's commit tx. func testHtlcTimeoutResolverExtractPreimageLocal(ht *lntest.HarnessTest) { - runMultiHopHtlcClaimTest(ht, runExtraPreimageFromLocalCommit) -} + // For neutrino backend there's no mempool source so we skip it. The + // test of extracting preimage from blocks has already been covered in + // other tests. + if ht.IsNeutrinoBackend() { + ht.Skip("skipping neutrino") + } -// runExtraPreimageFromLocalCommit checks that Bob's htlc timeout resolver will -// extract the preimage from the direct spend broadcast by Carol which spends -// the htlc output on Bob's commitment tx. -func runExtraPreimageFromLocalCommit(ht *lntest.HarnessTest, - alice, bob *node.HarnessNode, c lnrpc.CommitmentType, zeroConf bool) { + // Set the min relay feerate to be 10 sat/vbyte so the non-CPFP anchor + // is never swept. + // + // TODO(yy): delete this line once the normal anchor sweeping is + // removed. + ht.SetMinRelayFeerate(10_000) + + // Create a three hop network: Alice -> Bob -> Carol, using + // anchor channels. + // + // Prepare params. + params := lntest.OpenChannelParams{Amt: chanAmt} + cfg := node.CfgAnchor + cfgs := [][]string{cfg, cfg, cfg} // First, we'll create a three hop network: Alice -> Bob -> Carol, with // Carol refusing to actually settle or directly cancel any HTLC's // self. - aliceChanPoint, bobChanPoint, carol := createThreeHopNetwork( - ht, alice, bob, false, c, zeroConf, - ) - - // If this is a taproot channel, then we'll need to make some manual - // route hints so Alice can actually find a route. - var routeHints []*lnrpc.RouteHint - if c == lnrpc.CommitmentType_SIMPLE_TAPROOT { - routeHints = makeRouteHints(bob, carol, zeroConf) - } + // Create a three hop network: Alice -> Bob -> Carol. + chanPoints, nodes := ht.CreateSimpleNetwork(cfgs, params) + alice, bob, carol := nodes[0], nodes[1], nodes[2] + aliceChanPoint, bobChanPoint := chanPoints[0], chanPoints[1] // With the network active, we'll now add a new hodl invoice at Carol's // end. Make sure the cltv expiry delta is large enough, otherwise Bob @@ -513,10 +519,12 @@ func runExtraPreimageFromLocalCommit(ht *lntest.HarnessTest, Value: 100_000, CltvExpiry: finalCltvDelta, Hash: payHash[:], - RouteHints: routeHints, } carolInvoice := carol.RPC.AddHoldInvoice(invoiceReq) + // Record the height which the invoice will expire. + invoiceExpiry := ht.CurrentHeight() + uint32(invoiceReq.CltvExpiry) + // Subscribe the invoice. stream := carol.RPC.SubscribeSingleInvoice(payHash[:]) @@ -536,7 +544,7 @@ func runExtraPreimageFromLocalCommit(ht *lntest.HarnessTest, // Bob should have two HTLCs active. One incoming HTLC from Alice, and // one outgoing to Carol. ht.AssertIncomingHTLCActive(bob, aliceChanPoint, payHash[:]) - htlc := ht.AssertOutgoingHTLCActive(bob, bobChanPoint, payHash[:]) + ht.AssertOutgoingHTLCActive(bob, bobChanPoint, payHash[:]) // Carol should have one incoming HTLC from Bob. ht.AssertIncomingHTLCActive(carol, bobChanPoint, payHash[:]) @@ -564,114 +572,69 @@ func runExtraPreimageFromLocalCommit(ht *lntest.HarnessTest, // mempool. ht.CloseChannelAssertPending(bob, bobChanPoint, true) - // Bob should now has offered his anchors to his sweeper - both local - // and remote versions. - ht.AssertNumPendingSweeps(bob, 2) - // Mine Bob's force close tx. - closeTx := ht.MineClosingTx(bobChanPoint) - - // Mine Bob's anchor sweeping tx. - ht.MineBlocksAndAssertNumTxes(1, 1) - blocksMined := 1 - - // We'll now mine enough blocks to trigger Carol's sweeping of the htlc - // via the direct spend. With the default incoming broadcast delta of - // 10, this will be the htlc expiry height minus 10. - // - // NOTE: we need to mine 1 fewer block as we've already mined one to - // confirm Bob's force close tx. - numBlocks := padCLTV(uint32( - invoiceReq.CltvExpiry - lncfg.DefaultIncomingBroadcastDelta - 1, - )) + ht.MineClosingTx(bobChanPoint) - // If this is a nont script-enforced channel, Bob will be able to sweep - // his commit output after 4 blocks. - if c != lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE { - // Mine 3 blocks so the output will be offered to the sweeper. - ht.MineEmptyBlocks(defaultCSV - blocksMined - 1) + // Once Bob's force closing tx is confirmed, he will re-offer the + // anchor output to his sweeper, which won't be swept due to it being + // uneconomical. + ht.AssertNumPendingSweeps(bob, 1) - // Assert the commit output has been offered to the sweeper. - ht.AssertNumPendingSweeps(bob, 1) + // Mine 3 blocks so the output will be offered to the sweeper. + ht.MineBlocks(defaultCSV - 1) - // Mine a block to trigger the sweep. - ht.MineEmptyBlocks(1) - blocksMined = defaultCSV - } + // Bob should have two pending sweeps now, + // - the commit output. + // - the anchor output, uneconomical. + ht.AssertNumPendingSweeps(bob, 2) - // Mine empty blocks so it's easier to check Bob's sweeping txes below. - ht.MineEmptyBlocks(int(numBlocks) - blocksMined) + // Mine a block to confirm Bob's sweeping tx. + ht.MineBlocksAndAssertNumTxes(1, 1) - // With the above blocks mined, we should expect Carol's to offer the - // htlc output on Bob's commitment to the sweeper. - // - // TODO(yy): it's not offered to the sweeper yet, instead, the utxo - // nursery is creating and broadcasting the sweep tx - we should unify - // this behavior and offer it to the sweeper. - // ht.AssertNumPendingSweeps(carol, 1) + ht.Logf("Invoice expire height: %d, current: %d", invoiceExpiry, + ht.CurrentHeight()) - // Increase the fee rate used by the sweeper so Carol's direct spend tx - // won't be replaced by Bob's timeout tx. - ht.SetFeeEstimate(30000) + // We'll now mine enough blocks to trigger Carol's sweeping of the htlc + // via the direct spend. + numBlocks := padCLTV( + invoiceExpiry - ht.CurrentHeight() - incomingBroadcastDelta, + ) + ht.MineBlocks(int(numBlocks)) // Restart Carol to sweep the htlc output. require.NoError(ht, restartCarol()) + // With the above blocks mined, we should expect Carol's to offer the + // htlc output on Bob's commitment to the sweeper. + // + // Carol should two pending sweeps, + // - htlc output. + // - anchor output, uneconomical. ht.AssertNumPendingSweeps(carol, 2) - ht.MineEmptyBlocks(1) - - // Construct the htlc output on Bob's commitment tx, and decide its - // index based on the commit type below. - htlcOutpoint := wire.OutPoint{Hash: closeTx.TxHash()} // Check the current mempool state and we should see, - // - Carol's direct spend tx. - // - Bob's local output sweep tx, if this is NOT script enforced lease. + // - Carol's direct spend tx, which contains the preimage. // - Carol's anchor sweep tx cannot be broadcast as it's uneconomical. - switch c { - case lnrpc.CommitmentType_ANCHORS, lnrpc.CommitmentType_SIMPLE_TAPROOT: - htlcOutpoint.Index = 2 - ht.AssertNumTxsInMempool(2) - - case lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE: - htlcOutpoint.Index = 2 - ht.AssertNumTxsInMempool(1) - } - - // Get the current height to compute number of blocks to mine to - // trigger the timeout resolver from Bob. - height := ht.CurrentHeight() + ht.AssertNumTxsInMempool(1) // We'll now mine enough blocks to trigger Bob's htlc timeout resolver // to act. Once his timeout resolver starts, it will extract the // preimage from Carol's direct spend tx found in the mempool. - numBlocks = htlc.ExpirationHeight - height - - lncfg.DefaultOutgoingBroadcastDelta + resp := ht.AssertNumPendingForceClose(bob, 1)[0] + require.Equal(ht, 1, len(resp.PendingHtlcs)) - // Decrease the fee rate used by the sweeper so Bob's timeout tx will - // not replace Carol's direct spend tx. - ht.SetFeeEstimate(1000) + ht.Logf("Bob's timelock to_local output=%v, timelock on second stage "+ + "htlc=%v", resp.BlocksTilMaturity, + resp.PendingHtlcs[0].BlocksTilMaturity) // Mine empty blocks so Carol's direct spend tx stays in mempool. Once // the height is reached, Bob's timeout resolver will resolve the htlc // by extracing the preimage from the mempool. - ht.MineEmptyBlocks(int(numBlocks)) - - // For neutrino backend, the timeout resolver needs to extract the - // preimage from the blocks. - if ht.IsNeutrinoBackend() { - // Make sure the direct spend tx is still in the mempool. - ht.AssertOutpointInMempool(htlcOutpoint) - - // Mine a block to confirm two txns, - // - Carol's direct spend tx. - // - Bob's to_local output sweep tx. - if c != lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE { - ht.MineBlocksAndAssertNumTxes(1, 2) - } else { - ht.MineBlocksAndAssertNumTxes(1, 1) - } - } + // + // TODO(yy): there's no need to wait till the HTLC's CLTV is reached, + // Bob's outgoing contest resolver can also monitor the mempool and + // resolve the payment even earlier. + ht.MineEmptyBlocks(int(resp.PendingHtlcs[0].BlocksTilMaturity)) // Finally, check that the Alice's payment is marked as succeeded as // Bob has settled the htlc using the preimage extracted from Carol's From 2f0256775eb7c66451e16fd6546bacf4d7bd1eaa Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 24 Oct 2024 00:28:56 +0800 Subject: [PATCH 79/92] itest: flatten `testHtlcTimeoutResolverExtractPreimageRemote` Also remove unused code. --- itest/lnd_multi-hop_test.go | 340 ++++-------------------------------- 1 file changed, 37 insertions(+), 303 deletions(-) diff --git a/itest/lnd_multi-hop_test.go b/itest/lnd_multi-hop_test.go index ff37dae22b1..71a44966ed4 100644 --- a/itest/lnd_multi-hop_test.go +++ b/itest/lnd_multi-hop_test.go @@ -1,10 +1,6 @@ package itest import ( - "context" - "fmt" - "testing" - "github.com/btcsuite/btcd/btcutil" "github.com/lightningnetwork/lnd/chainreg" "github.com/lightningnetwork/lnd/lncfg" @@ -13,7 +9,6 @@ import ( "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/lntest" "github.com/lightningnetwork/lnd/lntest/node" - "github.com/lightningnetwork/lnd/lntest/rpc" "github.com/lightningnetwork/lnd/routing" "github.com/stretchr/testify/require" ) @@ -23,36 +18,6 @@ const ( thawHeightDelta = finalCltvDelta * 2 // 36. ) -var commitWithZeroConf = []struct { - commitType lnrpc.CommitmentType - zeroConf bool -}{ - { - commitType: lnrpc.CommitmentType_ANCHORS, - zeroConf: false, - }, - { - commitType: lnrpc.CommitmentType_ANCHORS, - zeroConf: true, - }, - { - commitType: lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE, - zeroConf: false, - }, - { - commitType: lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE, - zeroConf: true, - }, - { - commitType: lnrpc.CommitmentType_SIMPLE_TAPROOT, - zeroConf: false, - }, - { - commitType: lnrpc.CommitmentType_SIMPLE_TAPROOT, - zeroConf: true, - }, -} - // makeRouteHints creates a route hints that will allow Carol to be reached // using an unadvertised channel created by Bob (Bob -> Carol). If the zeroConf // bool is set, then the scid alias of Bob will be used in place. @@ -106,232 +71,42 @@ func makeRouteHints(bob, carol *node.HarnessNode, } } -// caseRunner defines a single test case runner. -type caseRunner func(ht *lntest.HarnessTest, alice, bob *node.HarnessNode, - c lnrpc.CommitmentType, zeroConf bool) - -// runMultiHopHtlcClaimTest is a helper method to build test cases based on -// different commitment types and zero-conf config and run them. -// -// TODO(yy): flatten this test. -func runMultiHopHtlcClaimTest(ht *lntest.HarnessTest, tester caseRunner) { - for _, typeAndConf := range commitWithZeroConf { - typeAndConf := typeAndConf - name := fmt.Sprintf("zeroconf=%v/committype=%v", - typeAndConf.zeroConf, typeAndConf.commitType.String()) - - // Create the nodes here so that separate logs will be created - // for Alice and Bob. - args := lntest.NodeArgsForCommitType(typeAndConf.commitType) - if typeAndConf.zeroConf { - args = append( - args, "--protocol.option-scid-alias", - "--protocol.zero-conf", - ) - } - - s := ht.Run(name, func(t1 *testing.T) { - st := ht.Subtest(t1) - - alice := st.NewNode("Alice", args) - bob := st.NewNode("Bob", args) - st.ConnectNodes(alice, bob) - - // Start each test with the default static fee estimate. - st.SetFeeEstimate(12500) - - // Add test name to the logs. - alice.AddToLogf("Running test case: %s", name) - bob.AddToLogf("Running test case: %s", name) - - tester( - st, alice, bob, - typeAndConf.commitType, typeAndConf.zeroConf, - ) - }) - if !s { - return - } - } -} - -// createThreeHopNetwork creates a topology of `Alice -> Bob -> Carol`. -func createThreeHopNetwork(ht *lntest.HarnessTest, - alice, bob *node.HarnessNode, carolHodl bool, c lnrpc.CommitmentType, - zeroConf bool) (*lnrpc.ChannelPoint, - *lnrpc.ChannelPoint, *node.HarnessNode) { - - ht.EnsureConnected(alice, bob) - - // We'll create a new node "carol" and have Bob connect to her. - // If the carolHodl flag is set, we'll make carol always hold onto the - // HTLC, this way it'll force Bob to go to chain to resolve the HTLC. - carolFlags := lntest.NodeArgsForCommitType(c) - if carolHodl { - carolFlags = append(carolFlags, "--hodl.exit-settle") - } - - if zeroConf { - carolFlags = append( - carolFlags, "--protocol.option-scid-alias", - "--protocol.zero-conf", - ) - } - carol := ht.NewNode("Carol", carolFlags) - - ht.ConnectNodes(bob, carol) - - // Make sure there are enough utxos for anchoring. Because the anchor - // by itself often doesn't meet the dust limit, a utxo from the wallet - // needs to be attached as an additional input. This can still lead to - // a positively-yielding transaction. - for i := 0; i < 2; i++ { - ht.FundCoinsUnconfirmed(btcutil.SatoshiPerBitcoin, alice) - ht.FundCoinsUnconfirmed(btcutil.SatoshiPerBitcoin, bob) - ht.FundCoinsUnconfirmed(btcutil.SatoshiPerBitcoin, carol) - - // Mine 1 block to get the above coins confirmed. - ht.MineBlocksAndAssertNumTxes(1, 3) - } - - // We'll start the test by creating a channel between Alice and Bob, - // which will act as the first leg for out multi-hop HTLC. - const chanAmt = 1000000 - var aliceFundingShim *lnrpc.FundingShim - var thawHeight uint32 - if c == lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE { - minerHeight := ht.CurrentHeight() - thawHeight = minerHeight + thawHeightDelta - aliceFundingShim, _ = deriveFundingShim( - ht, alice, bob, chanAmt, thawHeight, true, c, - ) - } - - var privateChan bool - if c == lnrpc.CommitmentType_SIMPLE_TAPROOT { - privateChan = true - } - - aliceParams := lntest.OpenChannelParams{ - Private: privateChan, - Amt: chanAmt, - CommitmentType: c, - FundingShim: aliceFundingShim, - ZeroConf: zeroConf, - } - - // If the channel type is taproot, then use an explicit channel type to - // open it. - if c == lnrpc.CommitmentType_SIMPLE_TAPROOT { - aliceParams.CommitmentType = lnrpc.CommitmentType_SIMPLE_TAPROOT - } - - // We'll create a channel from Bob to Carol. After this channel is - // open, our topology looks like: A -> B -> C. - var bobFundingShim *lnrpc.FundingShim - if c == lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE { - bobFundingShim, _ = deriveFundingShim( - ht, bob, carol, chanAmt, thawHeight, true, c, - ) - } - - // Prepare params for Bob. - bobParams := lntest.OpenChannelParams{ - Amt: chanAmt, - Private: privateChan, - CommitmentType: c, - FundingShim: bobFundingShim, - ZeroConf: zeroConf, - } - - // If the channel type is taproot, then use an explicit channel type to - // open it. - if c == lnrpc.CommitmentType_SIMPLE_TAPROOT { - bobParams.CommitmentType = lnrpc.CommitmentType_SIMPLE_TAPROOT - } - - var ( - acceptStreamBob rpc.AcceptorClient - acceptStreamCarol rpc.AcceptorClient - cancelBob context.CancelFunc - cancelCarol context.CancelFunc - ) - - // If a zero-conf channel is being opened, the nodes are signalling the - // zero-conf feature bit. Setup a ChannelAcceptor for the fundee. - if zeroConf { - acceptStreamBob, cancelBob = bob.RPC.ChannelAcceptor() - go acceptChannel(ht.T, true, acceptStreamBob) - - acceptStreamCarol, cancelCarol = carol.RPC.ChannelAcceptor() - go acceptChannel(ht.T, true, acceptStreamCarol) - } - - // Open channels in batch to save blocks mined. - reqs := []*lntest.OpenChannelRequest{ - {Local: alice, Remote: bob, Param: aliceParams}, - {Local: bob, Remote: carol, Param: bobParams}, - } - resp := ht.OpenMultiChannelsAsync(reqs) - aliceChanPoint := resp[0] - bobChanPoint := resp[1] - - // Make sure alice and carol know each other's channels. - // - // We'll only do this though if it wasn't a private channel we opened - // earlier. - if !privateChan { - ht.AssertChannelInGraph(alice, bobChanPoint) - ht.AssertChannelInGraph(carol, aliceChanPoint) - } else { - // Otherwise, we want to wait for all the channels to be shown - // as active before we proceed. - ht.AssertChannelExists(alice, aliceChanPoint) - ht.AssertChannelExists(carol, bobChanPoint) - } - - // Remove the ChannelAcceptor for Bob and Carol. - if zeroConf { - cancelBob() - cancelCarol() - } - - return aliceChanPoint, bobChanPoint, carol -} - // testHtlcTimeoutResolverExtractPreimageRemote tests that in the multi-hop // setting, Alice->Bob->Carol, when Bob's outgoing HTLC is swept by Carol using // the 2nd level success tx2nd level success tx, Bob's timeout resolver will -// extract the preimage from the sweep tx found in mempool or blocks(for -// neutrino). The 2nd level success tx is broadcast by Carol and spends the -// outpoint on her commit tx. +// extract the preimage from the sweep tx found in mempool. The 2nd level +// success tx is broadcast by Carol and spends the outpoint on her commit tx. func testHtlcTimeoutResolverExtractPreimageRemote(ht *lntest.HarnessTest) { - runMultiHopHtlcClaimTest(ht, runExtraPreimageFromRemoteCommit) -} + // For neutrino backend there's no mempool source so we skip it. The + // test of extracting preimage from blocks has already been covered in + // other tests. + if ht.IsNeutrinoBackend() { + ht.Skip("skipping neutrino") + } -// runExtraPreimageFromRemoteCommit checks that Bob's htlc timeout resolver -// will extract the preimage from the 2nd level success tx broadcast by Carol -// which spends the htlc output on her commitment tx. -func runExtraPreimageFromRemoteCommit(ht *lntest.HarnessTest, - alice, bob *node.HarnessNode, c lnrpc.CommitmentType, zeroConf bool) { + // Set the min relay feerate to be 10 sat/vbyte so the non-CPFP anchor + // is never swept. + // + // TODO(yy): delete this line once the normal anchor sweeping is + // removed. + ht.SetMinRelayFeerate(10_000) + + // Create a three hop network: Alice -> Bob -> Carol, using + // anchor channels. + // + // Prepare params. + params := lntest.OpenChannelParams{Amt: chanAmt} + cfg := node.CfgAnchor + cfgs := [][]string{cfg, cfg, cfg} // First, we'll create a three hop network: Alice -> Bob -> Carol, with // Carol refusing to actually settle or directly cancel any HTLC's // self. - aliceChanPoint, bobChanPoint, carol := createThreeHopNetwork( - ht, alice, bob, false, c, zeroConf, - ) - - if ht.IsNeutrinoBackend() { - ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) - } + chanPoints, nodes := ht.CreateSimpleNetwork(cfgs, params) + alice, bob, carol := nodes[0], nodes[1], nodes[2] + aliceChanPoint, bobChanPoint := chanPoints[0], chanPoints[1] - // If this is a taproot channel, then we'll need to make some manual - // route hints so Alice can actually find a route. - var routeHints []*lnrpc.RouteHint - if c == lnrpc.CommitmentType_SIMPLE_TAPROOT { - routeHints = makeRouteHints(bob, carol, zeroConf) - } + ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) // With the network active, we'll now add a new hodl invoice at Carol's // end. Make sure the cltv expiry delta is large enough, otherwise Bob @@ -342,7 +117,6 @@ func runExtraPreimageFromRemoteCommit(ht *lntest.HarnessTest, Value: 100_000, CltvExpiry: finalCltvDelta, Hash: payHash[:], - RouteHints: routeHints, } eveInvoice := carol.RPC.AddHoldInvoice(invoiceReq) @@ -387,28 +161,21 @@ func runExtraPreimageFromRemoteCommit(ht *lntest.HarnessTest, // timeout. With the default incoming broadcast delta of 10, this // will be the htlc expiry height minus 10. numBlocks := padCLTV(uint32( - invoiceReq.CltvExpiry - lncfg.DefaultIncomingBroadcastDelta, + invoiceReq.CltvExpiry - incomingBroadcastDelta, )) - ht.MineEmptyBlocks(int(numBlocks)) - - // Carol's force close transaction should now be found in the mempool. - // If there are anchors, we also expect Carol's contractcourt to offer - // the anchors to her sweeper - one from the local commitment and the - // other from the remote. - ht.AssertNumPendingSweeps(carol, 2) + ht.MineBlocks(int(numBlocks)) - // We now mine a block to confirm Carol's closing transaction, which - // will trigger her sweeper to sweep her CPFP anchor sweeping. - ht.MineClosingTx(bobChanPoint) + // Mine the two txns made from Carol, + // - the force close tx. + // - the anchor sweeping tx. + ht.MineBlocksAndAssertNumTxes(1, 2) // With the closing transaction confirmed, we should expect Carol's - // HTLC success transaction to be offered to the sweeper along with her - // anchor output. - ht.AssertNumPendingSweeps(carol, 2) + // HTLC success transaction to be offered to the sweeper. along with her + // anchor output. Note that the anchor output is uneconomical to sweep. + ht.AssertNumPendingSweeps(carol, 1) - // Mine a block to trigger the sweep, and clean up the anchor sweeping - // tx. - ht.MineBlocksAndAssertNumTxes(1, 1) + // We should now have Carol's htlc success tx in the mempool. ht.AssertNumTxsInMempool(1) // Restart Bob. Once he finishes syncing the channel state, he should @@ -423,18 +190,6 @@ func runExtraPreimageFromRemoteCommit(ht *lntest.HarnessTest, numBlocks = htlc.ExpirationHeight - height - lncfg.DefaultOutgoingBroadcastDelta - // We should now have Carol's htlc success tx in the mempool. - numTxesMempool := 1 - ht.AssertNumTxsInMempool(numTxesMempool) - - // For neutrino backend, the timeout resolver needs to extract the - // preimage from the blocks. - if ht.IsNeutrinoBackend() { - // Mine a block to confirm Carol's 2nd level success tx. - ht.MineBlocksAndAssertNumTxes(1, 1) - numBlocks-- - } - // Mine empty blocks so Carol's htlc success tx stays in mempool. Once // the height is reached, Bob's timeout resolver will resolve the htlc // by extracing the preimage from the mempool. @@ -445,29 +200,8 @@ func runExtraPreimageFromRemoteCommit(ht *lntest.HarnessTest, // 2nd level success tx. ht.AssertPaymentStatus(alice, preimage, lnrpc.Payment_SUCCEEDED) - switch c { - // For anchor channel type, we should expect to see Bob's commit output - // and his anchor output be swept in a single tx in the mempool. - case lnrpc.CommitmentType_ANCHORS, lnrpc.CommitmentType_SIMPLE_TAPROOT: - numTxesMempool++ - - // For script-enforced leased channel, Bob's anchor sweep tx won't - // happen as it's not used for CPFP, hence no wallet utxo is used so - // it'll be uneconomical. - case lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE: - } - - // For neutrino backend, Carol's second-stage sweep should be offered - // to her sweeper. - if ht.IsNeutrinoBackend() { - ht.AssertNumPendingSweeps(carol, 1) - - // Mine a block to trigger the sweep. - ht.MineEmptyBlocks(1) - } - // Mine a block to clean the mempool. - ht.MineBlocksAndAssertNumTxes(1, numTxesMempool) + ht.MineBlocksAndAssertNumTxes(1, 2) // NOTE: for non-standby nodes there's no need to clean up the force // close as long as the mempool is cleaned. From 2adb3566683d20a3eb18c79a2a6576616df032db Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 24 Oct 2024 00:30:08 +0800 Subject: [PATCH 80/92] itest: rename file to reflect the tests --- .../{lnd_multi-hop_test.go => lnd_htlc_timeout_resolver_test.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename itest/{lnd_multi-hop_test.go => lnd_htlc_timeout_resolver_test.go} (100%) diff --git a/itest/lnd_multi-hop_test.go b/itest/lnd_htlc_timeout_resolver_test.go similarity index 100% rename from itest/lnd_multi-hop_test.go rename to itest/lnd_htlc_timeout_resolver_test.go From e1d5bbf171d6f3bd21f0e08a1bee501c8fb01015 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 24 Oct 2024 01:14:01 +0800 Subject: [PATCH 81/92] itest: remove unnecessary force close --- itest/lnd_payment_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/itest/lnd_payment_test.go b/itest/lnd_payment_test.go index 7fb5dfa42d7..4df148d95e6 100644 --- a/itest/lnd_payment_test.go +++ b/itest/lnd_payment_test.go @@ -335,9 +335,6 @@ func runTestPaymentHTLCTimeout(ht *lntest.HarnessTest, restartAlice bool) { // sweep her outgoing HTLC in next block. ht.MineBlocksAndAssertNumTxes(1, 1) - // Cleanup the channel. - ht.CleanupForceClose(alice) - // We expect the non-dust payment to marked as failed in Alice's // database and also from her stream. ht.AssertPaymentStatus(alice, preimage, lnrpc.Payment_FAILED) From 2494f1ed32ab65c098c8902b569b213a11c17e90 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 24 Oct 2024 21:31:15 +0800 Subject: [PATCH 82/92] itest: remove redundant block mining in `testFailingChannel` --- itest/list_on_test.go | 2 +- itest/lnd_channel_force_close_test.go | 20 +++++++------------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/itest/list_on_test.go b/itest/list_on_test.go index ac440ff5335..a54dbe1bd0d 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -214,7 +214,7 @@ var allTestCases = []*lntest.TestCase{ TestFunc: testChannelForceClosure, }, { - Name: "failing link", + Name: "failing channel", TestFunc: testFailingChannel, }, { diff --git a/itest/lnd_channel_force_close_test.go b/itest/lnd_channel_force_close_test.go index 81bdfad4316..03570a29391 100644 --- a/itest/lnd_channel_force_close_test.go +++ b/itest/lnd_channel_force_close_test.go @@ -16,7 +16,6 @@ import ( "github.com/lightningnetwork/lnd/lntest" "github.com/lightningnetwork/lnd/lntest/node" "github.com/lightningnetwork/lnd/lntest/wait" - "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/routing" "github.com/stretchr/testify/require" ) @@ -873,8 +872,6 @@ func padCLTV(cltv uint32) uint32 { // in the case where a counterparty tries to settle an HTLC with the wrong // preimage. func testFailingChannel(ht *lntest.HarnessTest) { - const paymentAmt = 10000 - chanAmt := lnd.MaxFundingAmount // We'll introduce Carol, which will settle any incoming invoice with a @@ -893,7 +890,7 @@ func testFailingChannel(ht *lntest.HarnessTest) { invoice := &lnrpc.Invoice{ Memo: "testing", RPreimage: preimage, - Value: paymentAmt, + Value: invoiceAmt, } resp := carol.RPC.AddInvoice(invoice) @@ -926,12 +923,12 @@ func testFailingChannel(ht *lntest.HarnessTest) { // Carol will use the correct preimage to resolve the HTLC on-chain. ht.AssertNumPendingSweeps(carol, 1) - // Bring down the fee rate estimation, otherwise the following sweep - // won't happen. - ht.SetFeeEstimate(chainfee.FeePerKwFloor) - - // Mine a block to trigger Carol's sweeper to broadcast the sweeping - // tx. + // Mine a block to trigger the sweep. This is needed because the + // preimage extraction logic from the link is not managed by the + // blockbeat, which means the preimage may be sent to the contest + // resolver after it's launched. + // + // TODO(yy): Expose blockbeat to the link layer. ht.MineEmptyBlocks(1) // Carol should have broadcast her sweeping tx. @@ -944,9 +941,6 @@ func testFailingChannel(ht *lntest.HarnessTest) { // Alice's should have one pending sweep request for her commit output. ht.AssertNumPendingSweeps(alice, 1) - // Mine a block to trigger the sweep. - ht.MineEmptyBlocks(1) - // Mine Alice's sweeping tx. ht.MineBlocksAndAssertNumTxes(1, 1) From 22b9350320d767f142567a3ecce35724ccbcc249 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 24 Oct 2024 21:36:35 +0800 Subject: [PATCH 83/92] itest: remove redunant block mining in `testChannelFundingWithUnstableUtxos` --- itest/lnd_funding_test.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/itest/lnd_funding_test.go b/itest/lnd_funding_test.go index 0b08da32b32..074e53ef7e4 100644 --- a/itest/lnd_funding_test.go +++ b/itest/lnd_funding_test.go @@ -1297,6 +1297,7 @@ func testChannelFundingWithUnstableUtxos(ht *lntest.HarnessTest) { // First, we'll create two new nodes that we'll use to open channel // between for this test. carol := ht.NewNode("carol", nil) + // We'll attempt at max 2 pending channels, so Dave will need to accept // two pending ones. dave := ht.NewNode("dave", []string{ @@ -1375,9 +1376,6 @@ func testChannelFundingWithUnstableUtxos(ht *lntest.HarnessTest) { // Make sure Carol sees her to_remote output from the force close tx. ht.AssertNumPendingSweeps(carol, 1) - // Mine one block to trigger the sweep transaction. - ht.MineEmptyBlocks(1) - // We need to wait for carol initiating the sweep of the to_remote // output of chanPoint2. utxo := ht.AssertNumUTXOsUnconfirmed(carol, 1)[0] @@ -1435,9 +1433,6 @@ func testChannelFundingWithUnstableUtxos(ht *lntest.HarnessTest) { // Make sure Carol sees her to_remote output from the force close tx. ht.AssertNumPendingSweeps(carol, 1) - // Mine one block to trigger the sweep transaction. - ht.MineEmptyBlocks(1) - // Wait for the to_remote sweep tx to show up in carol's wallet. ht.AssertNumUTXOsUnconfirmed(carol, 1) From a55408d4a14608e89fdc74195935ecefbe9f952d Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 24 Oct 2024 21:47:39 +0800 Subject: [PATCH 84/92] itest: remove redudant block in `testPsbtChanFundingWithUnstableUtxos` --- itest/lnd_psbt_test.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/itest/lnd_psbt_test.go b/itest/lnd_psbt_test.go index 438661138b8..a774b3aa70d 100644 --- a/itest/lnd_psbt_test.go +++ b/itest/lnd_psbt_test.go @@ -1696,9 +1696,6 @@ func testPsbtChanFundingWithUnstableUtxos(ht *lntest.HarnessTest) { // Make sure Carol sees her to_remote output from the force close tx. ht.AssertNumPendingSweeps(carol, 1) - // Mine one block to trigger the sweep transaction. - ht.MineEmptyBlocks(1) - // We wait for the to_remote sweep tx. ht.AssertNumUTXOsUnconfirmed(carol, 1) @@ -1821,9 +1818,6 @@ func testPsbtChanFundingWithUnstableUtxos(ht *lntest.HarnessTest) { // Make sure Carol sees her to_remote output from the force close tx. ht.AssertNumPendingSweeps(carol, 1) - // Mine one block to trigger the sweep transaction. - ht.MineEmptyBlocks(1) - // We wait for the to_remote sweep tx of channelPoint2. utxos := ht.AssertNumUTXOsUnconfirmed(carol, 1) From 9d4a60d613b9df13dbeac03bce42dc36d8f1397f Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 24 Oct 2024 23:58:20 +0800 Subject: [PATCH 85/92] itest: remove redundant blocks in channel backup tests --- itest/lnd_channel_backup_test.go | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/itest/lnd_channel_backup_test.go b/itest/lnd_channel_backup_test.go index 5140de5056d..86bcae80a8e 100644 --- a/itest/lnd_channel_backup_test.go +++ b/itest/lnd_channel_backup_test.go @@ -1482,7 +1482,6 @@ func assertTimeLockSwept(ht *lntest.HarnessTest, carol, dave *node.HarnessNode, expectedTxes := 1 // Mine a block to trigger the sweeps. - ht.MineBlocks(1) ht.AssertNumTxsInMempool(expectedTxes) // Carol should consider the channel pending force close (since she is @@ -1512,7 +1511,7 @@ func assertTimeLockSwept(ht *lntest.HarnessTest, carol, dave *node.HarnessNode, // The commit sweep resolver publishes the sweep tx at defaultCSV-1 and // we already mined one block after the commitment was published, and // one block to trigger Carol's sweeps, so take that into account. - ht.MineEmptyBlocks(1) + ht.MineBlocks(2) ht.AssertNumPendingSweeps(dave, 2) // Mine a block to trigger the sweeps. @@ -1615,8 +1614,6 @@ func assertDLPExecuted(ht *lntest.HarnessTest, // output and the other for her anchor. ht.AssertNumPendingSweeps(carol, 2) - // Mine a block to trigger the sweep. - ht.MineEmptyBlocks(1) ht.MineBlocksAndAssertNumTxes(1, 1) // Now the channel should be fully closed also from Carol's POV. @@ -1635,8 +1632,6 @@ func assertDLPExecuted(ht *lntest.HarnessTest, // output and the other for his anchor. ht.AssertNumPendingSweeps(dave, 2) - // Mine a block to trigger the sweep. - ht.MineEmptyBlocks(1) ht.MineBlocksAndAssertNumTxes(1, 1) // Now Dave should consider the channel fully closed. @@ -1652,10 +1647,6 @@ func assertDLPExecuted(ht *lntest.HarnessTest, ht.AssertNumPendingSweeps(dave, 1) } - // Mine one block to trigger the sweeper to sweep. - ht.MineEmptyBlocks(1) - blocksMined++ - // Expect one tx - the commitment sweep from Dave. For anchor // channels, we expect the two anchor sweeping txns to be // failed due they are uneconomical. @@ -1673,9 +1664,6 @@ func assertDLPExecuted(ht *lntest.HarnessTest, // commitmment was published, so take that into account. ht.MineEmptyBlocks(int(defaultCSV - blocksMined)) - // Mine one block to trigger the sweeper to sweep. - ht.MineEmptyBlocks(1) - // Carol should have two pending sweeps: // 1. her commit output. // 2. her anchor output, if this is anchor channel. From b7feeba008b1d7df0ea62734be53502d0038bd5f Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 29 Jun 2024 10:37:38 +0800 Subject: [PATCH 86/92] itest+lntest: fix channel force close test Also flatten the tests to make them easier to be maintained. --- itest/list_on_test.go | 8 +- itest/lnd_channel_force_close_test.go | 371 +++++++++----------------- lntest/harness.go | 19 +- lntest/harness_miner.go | 3 +- 4 files changed, 158 insertions(+), 243 deletions(-) diff --git a/itest/list_on_test.go b/itest/list_on_test.go index a54dbe1bd0d..d9b4e92174c 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -210,8 +210,12 @@ var allTestCases = []*lntest.TestCase{ TestFunc: testChannelUnsettledBalance, }, { - Name: "channel force closure", - TestFunc: testChannelForceClosure, + Name: "channel force closure anchor", + TestFunc: testChannelForceClosureAnchor, + }, + { + Name: "channel force closure simple taproot", + TestFunc: testChannelForceClosureSimpleTaproot, }, { Name: "failing channel", diff --git a/itest/lnd_channel_force_close_test.go b/itest/lnd_channel_force_close_test.go index 03570a29391..33311922b05 100644 --- a/itest/lnd_channel_force_close_test.go +++ b/itest/lnd_channel_force_close_test.go @@ -3,14 +3,12 @@ package itest import ( "bytes" "fmt" - "testing" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/go-errors/errors" "github.com/lightningnetwork/lnd" - "github.com/lightningnetwork/lnd/chainreg" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/lntest" @@ -20,97 +18,82 @@ import ( "github.com/stretchr/testify/require" ) -// testChannelForceClosure performs a test to exercise the behavior of "force" -// closing a channel or unilaterally broadcasting the latest local commitment -// state on-chain. The test creates a new channel between Alice and Carol, then -// force closes the channel after some cursory assertions. Within the test, a -// total of 3 + n transactions will be broadcast, representing the commitment -// transaction, a transaction sweeping the local CSV delayed output, a -// transaction sweeping the CSV delayed 2nd-layer htlcs outputs, and n -// htlc timeout transactions, where n is the number of payments Alice attempted -// to send to Carol. This test includes several restarts to ensure that the -// transaction output states are persisted throughout the forced closure -// process. -// -// TODO(roasbeef): also add an unsettled HTLC before force closing. -func testChannelForceClosure(ht *lntest.HarnessTest) { - // We'll test the scenario for some of the commitment types, to ensure - // outputs can be swept. - commitTypes := []lnrpc.CommitmentType{ - lnrpc.CommitmentType_ANCHORS, - lnrpc.CommitmentType_SIMPLE_TAPROOT, +const pushAmt = btcutil.Amount(5e5) + +// testChannelForceClosureAnchor runs `runChannelForceClosureTest` with anchor +// channels. +func testChannelForceClosureAnchor(ht *lntest.HarnessTest) { + // Create a simple network: Alice -> Carol, using anchor channels. + // + // Prepare params. + openChannelParams := lntest.OpenChannelParams{ + Amt: chanAmt, + PushAmt: pushAmt, + CommitmentType: lnrpc.CommitmentType_ANCHORS, } - for _, channelType := range commitTypes { - testName := fmt.Sprintf("committype=%v", channelType) - - channelType := channelType - success := ht.Run(testName, func(t *testing.T) { - st := ht.Subtest(t) - - args := lntest.NodeArgsForCommitType(channelType) - alice := st.NewNode("Alice", args) - defer st.Shutdown(alice) - - // Since we'd like to test failure scenarios with - // outstanding htlcs, we'll introduce another node into - // our test network: Carol. - carolArgs := []string{"--hodl.exit-settle"} - carolArgs = append(carolArgs, args...) - carol := st.NewNode("Carol", carolArgs) - defer st.Shutdown(carol) - - // Each time, we'll send Alice new set of coins in - // order to fund the channel. - st.FundCoins(btcutil.SatoshiPerBitcoin, alice) - - // NOTE: Alice needs 3 more UTXOs to sweep her - // second-layer txns after a restart - after a restart - // all the time-sensitive sweeps are swept immediately - // without being aggregated. - // - // TODO(yy): remove this once the can recover its state - // from restart. - st.FundCoins(btcutil.SatoshiPerBitcoin, alice) - st.FundCoins(btcutil.SatoshiPerBitcoin, alice) - st.FundCoins(btcutil.SatoshiPerBitcoin, alice) - st.FundCoins(btcutil.SatoshiPerBitcoin, alice) - - // Also give Carol some coins to allow her to sweep her - // anchor. - st.FundCoins(btcutil.SatoshiPerBitcoin, carol) - - channelForceClosureTest(st, alice, carol, channelType) - }) - if !success { - return - } + cfg := node.CfgAnchor + cfgCarol := append([]string{"--hodl.exit-settle"}, cfg...) + cfgs := [][]string{cfg, cfgCarol} + + runChannelForceClosureTest(ht, cfgs, openChannelParams) +} + +// testChannelForceClosureSimpleTaproot runs `runChannelForceClosureTest` with +// simple taproot channels. +func testChannelForceClosureSimpleTaproot(ht *lntest.HarnessTest) { + // Create a simple network: Alice -> Carol, using simple taproot + // channels. + // + // Prepare params. + openChannelParams := lntest.OpenChannelParams{ + Amt: chanAmt, + PushAmt: pushAmt, + // If the channel is a taproot channel, then we'll need to + // create a private channel. + // + // TODO(roasbeef): lift after G175 + CommitmentType: lnrpc.CommitmentType_SIMPLE_TAPROOT, + Private: true, } + + cfg := node.CfgSimpleTaproot + cfgCarol := append([]string{"--hodl.exit-settle"}, cfg...) + cfgs := [][]string{cfg, cfgCarol} + + runChannelForceClosureTest(ht, cfgs, openChannelParams) } -func channelForceClosureTest(ht *lntest.HarnessTest, - alice, carol *node.HarnessNode, channelType lnrpc.CommitmentType) { +// runChannelForceClosureTest performs a test to exercise the behavior of +// "force" closing a channel or unilaterally broadcasting the latest local +// commitment state on-chain. The test creates a new channel between Alice and +// Carol, then force closes the channel after some cursory assertions. Within +// the test, a total of 3 + n transactions will be broadcast, representing the +// commitment transaction, a transaction sweeping the local CSV delayed output, +// a transaction sweeping the CSV delayed 2nd-layer htlcs outputs, and n htlc +// timeout transactions, where n is the number of payments Alice attempted +// to send to Carol. This test includes several restarts to ensure that the +// transaction output states are persisted throughout the forced closure +// process. +func runChannelForceClosureTest(ht *lntest.HarnessTest, + cfgs [][]string, params lntest.OpenChannelParams) { const ( - chanAmt = btcutil.Amount(10e6) - pushAmt = btcutil.Amount(5e6) - paymentAmt = 100000 - numInvoices = 6 + numInvoices = 6 + commitFeeRate = 20000 ) - const commitFeeRate = 20000 ht.SetFeeEstimate(commitFeeRate) - // TODO(roasbeef): should check default value in config here - // instead, or make delay a param - defaultCLTV := uint32(chainreg.DefaultBitcoinTimeLockDelta) - - // We must let Alice have an open channel before she can send a node - // announcement, so we open a channel with Carol, - ht.ConnectNodes(alice, carol) + // Create a three hop network: Alice -> Carol. + chanPoints, nodes := ht.CreateSimpleNetwork(cfgs, params) + alice, carol := nodes[0], nodes[1] + chanPoint := chanPoints[0] // We need one additional UTXO for sweeping the remote anchor. - ht.FundCoins(btcutil.SatoshiPerBitcoin, alice) + if ht.IsNeutrinoBackend() { + ht.FundCoins(btcutil.SatoshiPerBitcoin, alice) + } // Before we start, obtain Carol's current wallet balance, we'll check // to ensure that at the end of the force closure by Alice, Carol @@ -118,24 +101,6 @@ func channelForceClosureTest(ht *lntest.HarnessTest, carolBalResp := carol.RPC.WalletBalance() carolStartingBalance := carolBalResp.ConfirmedBalance - // If the channel is a taproot channel, then we'll need to create a - // private channel. - // - // TODO(roasbeef): lift after G175 - var privateChan bool - if channelType == lnrpc.CommitmentType_SIMPLE_TAPROOT { - privateChan = true - } - - chanPoint := ht.OpenChannel( - alice, carol, lntest.OpenChannelParams{ - Private: privateChan, - Amt: chanAmt, - PushAmt: pushAmt, - CommitmentType: channelType, - }, - ) - // Send payments from Alice to Carol, since Carol is htlchodl mode, the // htlc outputs should be left unsettled, and should be swept by the // utxo nursery. @@ -145,7 +110,7 @@ func channelForceClosureTest(ht *lntest.HarnessTest, Dest: carolPubKey, Amt: int64(paymentAmt), PaymentHash: ht.Random32Bytes(), - FinalCltvDelta: chainreg.DefaultBitcoinTimeLockDelta, + FinalCltvDelta: finalCltvDelta, TimeoutSeconds: 60, FeeLimitMsat: noFeeLimitMsat, } @@ -162,13 +127,13 @@ func channelForceClosureTest(ht *lntest.HarnessTest, curHeight := int32(ht.CurrentHeight()) // Using the current height of the chain, derive the relevant heights - // for incubating two-stage htlcs. + // for sweeping two-stage htlcs. var ( startHeight = uint32(curHeight) commCsvMaturityHeight = startHeight + 1 + defaultCSV - htlcExpiryHeight = padCLTV(startHeight + defaultCLTV) + htlcExpiryHeight = padCLTV(startHeight + finalCltvDelta) htlcCsvMaturityHeight = padCLTV( - startHeight + defaultCLTV + 1 + defaultCSV, + startHeight + finalCltvDelta + 1 + defaultCSV, ) ) @@ -199,21 +164,15 @@ func channelForceClosureTest(ht *lntest.HarnessTest, ) // The several restarts in this test are intended to ensure that when a - // channel is force-closed, the UTXO nursery has persisted the state of - // the channel in the closure process and will recover the correct + // channel is force-closed, the contract court has persisted the state + // of the channel in the closure process and will recover the correct // state when the system comes back on line. This restart tests state // persistence at the beginning of the process, when the commitment // transaction has been broadcast but not yet confirmed in a block. ht.RestartNode(alice) - // To give the neutrino backend some time to catch up with the chain, - // we wait here until we have enough UTXOs to actually sweep the local - // and remote anchor. - const expectedUtxos = 6 - ht.AssertNumUTXOs(alice, expectedUtxos) - // We expect to see Alice's force close tx in the mempool. - ht.GetNumTxsFromMempool(1) + ht.AssertNumTxsInMempool(1) // Mine a block which should confirm the commitment transaction // broadcast as a result of the force closure. Once mined, we also @@ -258,46 +217,34 @@ func channelForceClosureTest(ht *lntest.HarnessTest, // The following restart is intended to ensure that outputs from the // force close commitment transaction have been persisted once the - // transaction has been confirmed, but before the outputs are spendable - // (the "kindergarten" bucket.) + // transaction has been confirmed, but before the outputs are + // spendable. ht.RestartNode(alice) // Carol should offer her commit and anchor outputs to the sweeper. sweepTxns := ht.AssertNumPendingSweeps(carol, 2) - // Find Carol's anchor sweep. + // Identify Carol's pending sweeps. var carolAnchor, carolCommit = sweepTxns[0], sweepTxns[1] if carolAnchor.AmountSat != uint32(anchorSize) { carolAnchor, carolCommit = carolCommit, carolAnchor } - // Mine a block to trigger Carol's sweeper to make decisions on the - // anchor sweeping. - ht.MineEmptyBlocks(1) - // Carol's sweep tx should be in the mempool already, as her output is - // not timelocked. + // not timelocked. This sweep tx should spend her to_local output as + // the anchor output is not economical to spend. carolTx := ht.GetNumTxsFromMempool(1)[0] - // Carol's sweeping tx should have 2-input-1-output shape. - require.Len(ht, carolTx.TxIn, 2) + // Carol's sweeping tx should have 1-input-1-output shape. + require.Len(ht, carolTx.TxIn, 1) require.Len(ht, carolTx.TxOut, 1) // Calculate the total fee Carol paid. totalFeeCarol := ht.CalculateTxFee(carolTx) - // If we have anchors, add an anchor resolution for carol. - op := fmt.Sprintf("%v:%v", carolAnchor.Outpoint.TxidStr, - carolAnchor.Outpoint.OutputIndex) - carolReports[op] = &lnrpc.Resolution{ - ResolutionType: lnrpc.ResolutionType_ANCHOR, - Outcome: lnrpc.ResolutionOutcome_CLAIMED, - SweepTxid: carolTx.TxHash().String(), - AmountSat: anchorSize, - Outpoint: carolAnchor.Outpoint, - } - - op = fmt.Sprintf("%v:%v", carolCommit.Outpoint.TxidStr, + // Carol's anchor report won't be created since it's uneconomical to + // sweep. So we expect to see only the commit sweep report. + op := fmt.Sprintf("%v:%v", carolCommit.Outpoint.TxidStr, carolCommit.Outpoint.OutputIndex) carolReports[op] = &lnrpc.Resolution{ ResolutionType: lnrpc.ResolutionType_COMMIT, @@ -319,9 +266,9 @@ func channelForceClosureTest(ht *lntest.HarnessTest, // Alice should still have the anchor sweeping request. ht.AssertNumPendingSweeps(alice, 1) - // The following restart checks to ensure that outputs in the - // kindergarten bucket are persisted while waiting for the required - // number of confirmations to be reported. + // The following restart checks to ensure that outputs in the contract + // court are persisted while waiting for the required number of + // confirmations to be reported. ht.RestartNode(alice) // Alice should see the channel in her set of pending force closed @@ -344,12 +291,12 @@ func channelForceClosureTest(ht *lntest.HarnessTest, aliceBalance = forceClose.Channel.LocalBalance // At this point, the nursery should show that the commitment - // output has 2 block left before its CSV delay expires. In + // output has 3 block left before its CSV delay expires. In // total, we have mined exactly defaultCSV blocks, so the htlc // outputs should also reflect that this many blocks have // passed. err = checkCommitmentMaturity( - forceClose, commCsvMaturityHeight, 2, + forceClose, commCsvMaturityHeight, 3, ) if err != nil { return err @@ -368,9 +315,9 @@ func channelForceClosureTest(ht *lntest.HarnessTest, }, defaultTimeout) require.NoError(ht, err, "timeout while checking force closed channel") - // Generate an additional block, which should cause the CSV delayed - // output from the commitment txn to expire. - ht.MineEmptyBlocks(1) + // Generate two blocks, which should cause the CSV delayed output from + // the commitment txn to expire. + ht.MineBlocks(2) // At this point, the CSV will expire in the next block, meaning that // the output should be offered to the sweeper. @@ -380,14 +327,9 @@ func channelForceClosureTest(ht *lntest.HarnessTest, commitSweep, anchorSweep = anchorSweep, commitSweep } - // Restart Alice to ensure that she resumes watching the finalized - // commitment sweep txid. - ht.RestartNode(alice) - // Mine one block and the sweeping transaction should now be broadcast. // So we fetch the node's mempool to ensure it has been properly // broadcast. - ht.MineEmptyBlocks(1) sweepingTXID := ht.AssertNumTxsInMempool(1)[0] // Fetch the sweep transaction, all input it's spending should be from @@ -398,7 +340,12 @@ func channelForceClosureTest(ht *lntest.HarnessTest, "sweep transaction not spending from commit") } - // We expect a resolution which spends our commit output. + // Restart Alice to ensure that she resumes watching the finalized + // commitment sweep txid. + ht.RestartNode(alice) + + // Alice's anchor report won't be created since it's uneconomical to + // sweep. We expect a resolution which spends our commit output. op = fmt.Sprintf("%v:%v", commitSweep.Outpoint.TxidStr, commitSweep.Outpoint.OutputIndex) aliceReports[op] = &lnrpc.Resolution{ @@ -409,17 +356,6 @@ func channelForceClosureTest(ht *lntest.HarnessTest, AmountSat: uint64(aliceBalance), } - // Add alice's anchor to our expected set of reports. - op = fmt.Sprintf("%v:%v", aliceAnchor.Outpoint.TxidStr, - aliceAnchor.Outpoint.OutputIndex) - aliceReports[op] = &lnrpc.Resolution{ - ResolutionType: lnrpc.ResolutionType_ANCHOR, - Outcome: lnrpc.ResolutionOutcome_CLAIMED, - SweepTxid: sweepingTXID.String(), - Outpoint: aliceAnchor.Outpoint, - AmountSat: uint64(anchorSize), - } - // Check that we can find the commitment sweep in our set of known // sweeps, using the simple transaction id ListSweeps output. ht.AssertSweepFound(alice, sweepingTXID.String(), false, 0) @@ -489,17 +425,13 @@ func channelForceClosureTest(ht *lntest.HarnessTest, // Advance the blockchain until just before the CLTV expires, nothing // exciting should have happened during this time. - ht.MineEmptyBlocks(cltvHeightDelta) + ht.MineBlocks(cltvHeightDelta) // We now restart Alice, to ensure that she will broadcast the // presigned htlc timeout txns after the delay expires after // experiencing a while waiting for the htlc outputs to incubate. ht.RestartNode(alice) - // To give the neutrino backend some time to catch up with the chain, - // we wait here until we have enough UTXOs to - // ht.AssertNumUTXOs(alice, expectedUtxos) - // Alice should now see the channel in her set of pending force closed // channels with one pending HTLC. err = wait.NoError(func() error { @@ -534,24 +466,23 @@ func channelForceClosureTest(ht *lntest.HarnessTest, // Now, generate the block which will cause Alice to offer the // presigned htlc timeout txns to the sweeper. - ht.MineEmptyBlocks(1) + ht.MineBlocks(1) // Since Alice had numInvoices (6) htlcs extended to Carol before force // closing, we expect Alice to broadcast an htlc timeout txn for each - // one. - ht.AssertNumPendingSweeps(alice, numInvoices) + // one. We also expect Alice to still have her anchor since it's not + // swept. + ht.AssertNumPendingSweeps(alice, numInvoices+1) // Wait for them all to show up in the mempool - // - // NOTE: after restart, all the htlc timeout txns will be offered to - // the sweeper with `Immediate` set to true, so they won't be - // aggregated. - htlcTxIDs := ht.AssertNumTxsInMempool(numInvoices) + htlcTxIDs := ht.AssertNumTxsInMempool(1) // Retrieve each htlc timeout txn from the mempool, and ensure it is - // well-formed. This entails verifying that each only spends from - // output, and that output is from the commitment txn. - numInputs := 2 + // well-formed. The sweeping tx should spend all the htlc outputs. + // + // NOTE: We also add 1 output as the outgoing HTLC is swept using twice + // its value as its budget, so a wallet utxo is used. + numInputs := 6 + 1 // Construct a map of the already confirmed htlc timeout outpoints, // that will count the number of times each is spent by the sweep txn. @@ -560,6 +491,8 @@ func channelForceClosureTest(ht *lntest.HarnessTest, var htlcTxOutpointSet = make(map[wire.OutPoint]int) var htlcLessFees uint64 + + //nolint:ll for _, htlcTxID := range htlcTxIDs { // Fetch the sweep transaction, all input it's spending should // be from the commitment transaction which was broadcast @@ -652,10 +585,10 @@ func channelForceClosureTest(ht *lntest.HarnessTest, // Generate a block that mines the htlc timeout txns. Doing so now // activates the 2nd-stage CSV delayed outputs. - ht.MineBlocksAndAssertNumTxes(1, numInvoices) + ht.MineBlocksAndAssertNumTxes(1, 1) - // Alice is restarted here to ensure that she promptly moved the crib - // outputs to the kindergarten bucket after the htlc timeout txns were + // Alice is restarted here to ensure that her contract court properly + // handles the 2nd-stage sweeps after the htlc timeout txns were // confirmed. ht.RestartNode(alice) @@ -664,12 +597,19 @@ func channelForceClosureTest(ht *lntest.HarnessTest, currentHeight = int32(ht.CurrentHeight()) ht.Logf("current height: %v, htlcCsvMaturityHeight=%v", currentHeight, htlcCsvMaturityHeight) - numBlocks := int(htlcCsvMaturityHeight - uint32(currentHeight) - 2) - ht.MineEmptyBlocks(numBlocks) + numBlocks := int(htlcCsvMaturityHeight - uint32(currentHeight) - 1) + ht.MineBlocks(numBlocks) - // Restart Alice to ensure that she can recover from a failure before - // having graduated the htlc outputs in the kindergarten bucket. - ht.RestartNode(alice) + ht.AssertNumPendingSweeps(alice, numInvoices+1) + + // Restart Alice to ensure that she can recover from a failure. + // + // TODO(yy): Skip this step for neutrino as it cannot recover the + // sweeping txns from the mempool. We need to also store the txns in + // the sweeper store to make it work for the neutrino case. + if !ht.IsNeutrinoBackend() { + ht.RestartNode(alice) + } // Now that the channel has been fully swept, it should no longer show // incubated, check to see that Alice's node still reports the channel @@ -687,55 +627,13 @@ func channelForceClosureTest(ht *lntest.HarnessTest, }, defaultTimeout) require.NoError(ht, err, "timeout while checking force closed channel") - // Generate a block that causes Alice to sweep the htlc outputs in the - // kindergarten bucket. - ht.MineEmptyBlocks(1) - ht.AssertNumPendingSweeps(alice, numInvoices) - - // Mine a block to trigger the sweep. - ht.MineEmptyBlocks(1) - - // A temp hack to ensure the CI is not blocking the current - // development. There's a known issue in block sync among different - // subsystems, which is scheduled to be fixed in 0.18.1. - if ht.IsNeutrinoBackend() { - // We expect the htlcs to be aggregated into one tx. However, - // due to block sync issue, they may end up in two txns. Here - // we assert that there are two txns found in the mempool - if - // succeeded, it means the aggregation failed, and we won't - // continue the test. - // - // NOTE: we don't check `len(mempool) == 1` because it will - // give us false positive. - err := wait.NoError(func() error { - mempool := ht.Miner().GetRawMempool() - if len(mempool) == 2 { - return nil - } - - return fmt.Errorf("expected 2 txes in mempool, found "+ - "%d", len(mempool)) - }, lntest.DefaultTimeout) - ht.Logf("Assert num of txns got %v", err) - - // If there are indeed two txns found in the mempool, we won't - // continue the test. - if err == nil { - ht.Log("Neutrino backend failed to aggregate htlc " + - "sweeps!") - - // Clean the mempool. - ht.MineBlocksAndAssertNumTxes(1, 2) - - return - } - } + ht.AssertNumPendingSweeps(alice, numInvoices+1) // Wait for the single sweep txn to appear in the mempool. - htlcSweepTxID := ht.AssertNumTxsInMempool(1)[0] + htlcSweepTxid := ht.AssertNumTxsInMempool(1)[0] // Fetch the htlc sweep transaction from the mempool. - htlcSweepTx := ht.GetRawTransaction(htlcSweepTxID) + htlcSweepTx := ht.GetRawTransaction(htlcSweepTxid) // Ensure the htlc sweep transaction only has one input for each htlc // Alice extended before force closing. @@ -747,6 +645,7 @@ func channelForceClosureTest(ht *lntest.HarnessTest, // Ensure that each output spends from exactly one htlc timeout output. for _, txIn := range htlcSweepTx.MsgTx().TxIn { outpoint := txIn.PreviousOutPoint + // Check that the input is a confirmed htlc timeout txn. _, ok := htlcTxOutpointSet[outpoint] require.Truef(ht, ok, "htlc sweep output not spending from "+ @@ -784,11 +683,11 @@ func channelForceClosureTest(ht *lntest.HarnessTest, // Check that we can find the htlc sweep in our set of sweeps using // the verbose output of the listsweeps output. - ht.AssertSweepFound(alice, htlcSweepTx.Hash().String(), true, 0) + ht.AssertSweepFound(alice, htlcSweepTxid.String(), true, 0) - // The following restart checks to ensure that the nursery store is - // storing the txid of the previously broadcast htlc sweep txn, and - // that it begins watching that txid after restarting. + // The following restart checks to ensure that the sweeper is storing + // the txid of the previously broadcast htlc sweep txn, and that it + // begins watching that txid after restarting. ht.RestartNode(alice) // Now that the channel has been fully swept, it should no longer show @@ -804,7 +703,7 @@ func channelForceClosureTest(ht *lntest.HarnessTest, } err = checkPendingHtlcStageAndMaturity( - forceClose, 2, htlcCsvMaturityHeight-1, -1, + forceClose, 2, htlcCsvMaturityHeight-1, 0, ) if err != nil { return err @@ -817,7 +716,7 @@ func channelForceClosureTest(ht *lntest.HarnessTest, // Generate the final block that sweeps all htlc funds into the user's // wallet, and make sure the sweep is in this block. block := ht.MineBlocksAndAssertNumTxes(1, 1)[0] - ht.AssertTxInBlock(block, htlcSweepTxID) + ht.AssertTxInBlock(block, htlcSweepTxid) // Now that the channel has been fully swept, it should no longer show // up within the pending channels RPC. @@ -846,12 +745,6 @@ func channelForceClosureTest(ht *lntest.HarnessTest, carolExpectedBalance := btcutil.Amount(carolStartingBalance) + pushAmt - totalFeeCarol - // In addition, if this is an anchor-enabled channel, further add the - // anchor size. - if lntest.CommitTypeHasAnchors(channelType) { - carolExpectedBalance += btcutil.Amount(anchorSize) - } - require.Equal(ht, carolExpectedBalance, btcutil.Amount(carolBalResp.ConfirmedBalance), "carol's balance is incorrect") diff --git a/lntest/harness.go b/lntest/harness.go index 57602e5dfad..91331370e50 100644 --- a/lntest/harness.go +++ b/lntest/harness.go @@ -1660,6 +1660,22 @@ func (h *HarnessTest) CleanupForceClose(hn *node.HarnessNode) { // Wait for the channel to be marked pending force close. h.AssertNumPendingForceClose(hn, 1) + // Mine enough blocks for the node to sweep its funds from the force + // closed channel. The commit sweep resolver is offers the input to the + // sweeper when it's force closed, and broadcast the sweep tx at + // defaulCSV-1. + // + // NOTE: we might empty blocks here as we don't know the exact number + // of blocks to mine. This may end up mining more blocks than needed. + h.MineEmptyBlocks(node.DefaultCSV - 1) + + // Assert there is one pending sweep. + h.AssertNumPendingSweeps(hn, 1) + + // The node should now sweep the funds, clean up by mining the sweeping + // tx. + h.MineBlocksAndAssertNumTxes(1, 1) + // Mine blocks to get any second level HTLC resolved. If there are no // HTLCs, this will behave like h.AssertNumPendingCloseChannels. h.mineTillForceCloseResolved(hn) @@ -2001,7 +2017,8 @@ func (h *HarnessTest) AssertSweepFound(hn *node.HarnessNode, return nil } - return fmt.Errorf("sweep tx %v not found", sweep) + return fmt.Errorf("sweep tx %v not found in resp %v", sweep, + sweepResp) }, wait.DefaultTimeout) require.NoError(h, err, "%s: timeout checking sweep tx", hn.Name()) } diff --git a/lntest/harness_miner.go b/lntest/harness_miner.go index 22b2b95acc4..17fd864ed78 100644 --- a/lntest/harness_miner.go +++ b/lntest/harness_miner.go @@ -196,7 +196,8 @@ func (h *HarnessTest) mineTillForceCloseResolved(hn *node.HarnessNode) { return nil }, DefaultTimeout) - require.NoErrorf(h, err, "assert force close resolved timeout") + require.NoErrorf(h, err, "%s: assert force close resolved timeout", + hn.Name()) } // AssertTxInMempool asserts a given transaction can be found in the mempool. From 9c0c373b7e6f2e19189ec4c564898b15e523d21b Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 25 Oct 2024 14:05:18 +0800 Subject: [PATCH 87/92] itest: flatten and fix `testWatchtower` --- itest/list_on_test.go | 5 +--- itest/lnd_watchtower_test.go | 50 +++++++++++++++++------------------- 2 files changed, 25 insertions(+), 30 deletions(-) diff --git a/itest/list_on_test.go b/itest/list_on_test.go index d9b4e92174c..a192a423f44 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -550,10 +550,6 @@ var allTestCases = []*lntest.TestCase{ Name: "lookup htlc resolution", TestFunc: testLookupHtlcResolution, }, - { - Name: "watchtower", - TestFunc: testWatchtower, - }, { Name: "channel fundmax", TestFunc: testChannelFundMax, @@ -695,4 +691,5 @@ var allTestCases = []*lntest.TestCase{ func init() { // Register subtests. allTestCases = append(allTestCases, multiHopForceCloseTestCases...) + allTestCases = append(allTestCases, watchtowerTestCases...) } diff --git a/itest/lnd_watchtower_test.go b/itest/lnd_watchtower_test.go index 0a36b077d2d..80a0eae5a35 100644 --- a/itest/lnd_watchtower_test.go +++ b/itest/lnd_watchtower_test.go @@ -19,22 +19,21 @@ import ( "github.com/stretchr/testify/require" ) -// testWatchtower tests the behaviour of the watchtower client and server. -func testWatchtower(ht *lntest.HarnessTest) { - ht.Run("revocation", func(t *testing.T) { - tt := ht.Subtest(t) - testRevokedCloseRetributionAltruistWatchtower(tt) - }) - - ht.Run("session deletion", func(t *testing.T) { - tt := ht.Subtest(t) - testTowerClientSessionDeletion(tt) - }) - - ht.Run("tower and session activation", func(t *testing.T) { - tt := ht.Subtest(t) - testTowerClientTowerAndSessionManagement(tt) - }) +// watchtowerTestCases defines a set of tests to check the behaviour of the +// watchtower client and server. +var watchtowerTestCases = []*lntest.TestCase{ + { + Name: "watchtower revoked close retribution altruist", + TestFunc: testRevokedCloseRetributionAltruistWatchtower, + }, + { + Name: "watchtower client session deletion", + TestFunc: testTowerClientSessionDeletion, + }, + { + Name: "watchtower client tower and session management", + TestFunc: testTowerClientTowerAndSessionManagement, + }, } // testTowerClientTowerAndSessionManagement tests the various control commands @@ -565,6 +564,15 @@ func testRevokedCloseRetributionAltruistWatchtowerCase(ht *lntest.HarnessTest, // then been swept to his wallet by Willy. require.NoError(ht, restart(), "unable to restart dave") + // For neutrino backend, we may need to mine one more block to trigger + // the chain watcher to act. + // + // TODO(yy): remove it once the blockbeat remembers the last block + // processed. + if ht.IsNeutrinoBackend() { + ht.MineEmptyBlocks(1) + } + err = wait.NoError(func() error { daveBalResp := dave.RPC.ChannelBalance() if daveBalResp.LocalBalance.Sat != 0 { @@ -579,16 +587,6 @@ func testRevokedCloseRetributionAltruistWatchtowerCase(ht *lntest.HarnessTest, ht.AssertNumPendingForceClose(dave, 0) - // If this is an anchor channel, Dave would offer his sweeper the - // anchor. However, due to no time-sensitive outputs involved, the - // anchor sweeping won't happen as it's uneconomical. - if lntest.CommitTypeHasAnchors(commitType) { - ht.AssertNumPendingSweeps(dave, 1) - - // Mine a block to trigger the sweep. - ht.MineEmptyBlocks(1) - } - // Check that Dave's wallet balance is increased. err = wait.NoError(func() error { daveBalResp := dave.RPC.WalletBalance() From 425877e74565d0e7ca1edf68f79a9c96962a0f8c Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 25 Oct 2024 14:05:55 +0800 Subject: [PATCH 88/92] itest: remove redundant block in multiple tests --- itest/lnd_channel_backup_test.go | 14 +++++++++++--- itest/lnd_route_blinding_test.go | 7 ++++--- itest/lnd_wipe_fwdpkgs_test.go | 3 --- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/itest/lnd_channel_backup_test.go b/itest/lnd_channel_backup_test.go index 86bcae80a8e..d6e32d368f4 100644 --- a/itest/lnd_channel_backup_test.go +++ b/itest/lnd_channel_backup_test.go @@ -1320,12 +1320,20 @@ func testDataLossProtection(ht *lntest.HarnessTest) { // information Dave needs to sweep his funds. require.NoError(ht, restartDave(), "unable to restart Eve") + // Mine a block to trigger Dave's chain watcher to process Carol's sweep + // tx. + // + // TODO(yy): remove this block once the blockbeat starts remembering + // its last processed block and can handle looking for spends in the + // past blocks. + ht.MineEmptyBlocks(1) + + // Make sure Dave still has the pending force close channel. + ht.AssertNumPendingForceClose(dave, 1) + // Dave should have a pending sweep. ht.AssertNumPendingSweeps(dave, 1) - // Mine a block to trigger the sweep. - ht.MineBlocks(1) - // Dave should sweep his funds. ht.AssertNumTxsInMempool(1) diff --git a/itest/lnd_route_blinding_test.go b/itest/lnd_route_blinding_test.go index fd378a25f73..85690da7d3d 100644 --- a/itest/lnd_route_blinding_test.go +++ b/itest/lnd_route_blinding_test.go @@ -832,7 +832,6 @@ func testErrorHandlingOnChainFailure(ht *lntest.HarnessTest) { // we've already mined 1 block so we need one less than our CSV. ht.MineBlocks(node.DefaultCSV - 1) ht.AssertNumPendingSweeps(ht.Bob, 1) - ht.MineEmptyBlocks(1) ht.MineBlocksAndAssertNumTxes(1, 1) // Restart bob so that we can test that he's able to recover everything @@ -852,6 +851,7 @@ func testErrorHandlingOnChainFailure(ht *lntest.HarnessTest) { ht.AssertNumPendingSweeps(ht.Bob, 0) ht.MineBlocksAndAssertNumTxes(1, 1) + // Assert that the HTLC has cleared. ht.AssertHTLCNotActive(ht.Bob, testCase.channels[0], hash[:]) ht.AssertHTLCNotActive(ht.Alice, testCase.channels[0], hash[:]) @@ -866,8 +866,9 @@ func testErrorHandlingOnChainFailure(ht *lntest.HarnessTest) { ) // Clean up the rest of our force close: mine blocks so that Bob's CSV - // expires plus one block to trigger his sweep and then mine it. - ht.MineBlocks(node.DefaultCSV + 1) + // expires to trigger his sweep and then mine it. + ht.MineBlocks(node.DefaultCSV) + ht.AssertNumPendingSweeps(ht.Bob, 1) ht.MineBlocksAndAssertNumTxes(1, 1) // Bring carol back up so that we can close out the rest of our diff --git a/itest/lnd_wipe_fwdpkgs_test.go b/itest/lnd_wipe_fwdpkgs_test.go index a302f596a63..b6211d7f8e5 100644 --- a/itest/lnd_wipe_fwdpkgs_test.go +++ b/itest/lnd_wipe_fwdpkgs_test.go @@ -117,9 +117,6 @@ func testWipeForwardingPackages(ht *lntest.HarnessTest) { // Alice should one pending sweep. ht.AssertNumPendingSweeps(alice, 1) - // Mine a block to trigger the sweep. - ht.MineBlocks(1) - // Mine 1 block to get Alice's sweeping tx confirmed. ht.MineBlocksAndAssertNumTxes(1, 1) From 36a87ad5f477b57339cdce4b23f5b47080a5cdc5 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 25 Oct 2024 03:03:06 +0800 Subject: [PATCH 89/92] itest: assert payment status after sending --- itest/lnd_channel_balance_test.go | 2 +- itest/lnd_channel_force_close_test.go | 2 +- itest/lnd_forward_interceptor_test.go | 6 ++---- itest/lnd_hold_invoice_force_test.go | 2 +- itest/lnd_htlc_timeout_resolver_test.go | 4 ++-- itest/lnd_misc_test.go | 6 ++++-- itest/lnd_multi-hop_force_close_test.go | 28 +++++++++++++------------ itest/lnd_watchtower_test.go | 17 ++++++--------- 8 files changed, 32 insertions(+), 35 deletions(-) diff --git a/itest/lnd_channel_balance_test.go b/itest/lnd_channel_balance_test.go index 72dd16ea34b..8ab276d2e64 100644 --- a/itest/lnd_channel_balance_test.go +++ b/itest/lnd_channel_balance_test.go @@ -156,7 +156,7 @@ func testChannelUnsettledBalance(ht *lntest.HarnessTest) { TimeoutSeconds: 60, FeeLimitMsat: noFeeLimitMsat, } - alice.RPC.SendPayment(req) + ht.SendPaymentAssertInflight(alice, req) }() } diff --git a/itest/lnd_channel_force_close_test.go b/itest/lnd_channel_force_close_test.go index 33311922b05..e5ef6b7cab9 100644 --- a/itest/lnd_channel_force_close_test.go +++ b/itest/lnd_channel_force_close_test.go @@ -114,7 +114,7 @@ func runChannelForceClosureTest(ht *lntest.HarnessTest, TimeoutSeconds: 60, FeeLimitMsat: noFeeLimitMsat, } - alice.RPC.SendPayment(req) + ht.SendPaymentAssertInflight(alice, req) } // Once the HTLC has cleared, all the nodes n our mini network should diff --git a/itest/lnd_forward_interceptor_test.go b/itest/lnd_forward_interceptor_test.go index 0edf1e1098e..a58dc34154e 100644 --- a/itest/lnd_forward_interceptor_test.go +++ b/itest/lnd_forward_interceptor_test.go @@ -508,8 +508,7 @@ func testForwardInterceptorWireRecords(ht *lntest.HarnessTest) { FeeLimitMsat: noFeeLimitMsat, FirstHopCustomRecords: customRecords, } - - _ = alice.RPC.SendPayment(sendReq) + ht.SendPaymentAssertInflight(alice, sendReq) // We start the htlc interceptor with a simple implementation that saves // all intercepted packets. These packets are held to simulate a @@ -635,8 +634,7 @@ func testForwardInterceptorRestart(ht *lntest.HarnessTest) { FeeLimitMsat: noFeeLimitMsat, FirstHopCustomRecords: customRecords, } - - _ = alice.RPC.SendPayment(sendReq) + ht.SendPaymentAssertInflight(alice, sendReq) // We start the htlc interceptor with a simple implementation that saves // all intercepted packets. These packets are held to simulate a diff --git a/itest/lnd_hold_invoice_force_test.go b/itest/lnd_hold_invoice_force_test.go index 7c616e7a50c..8cdd8a5298f 100644 --- a/itest/lnd_hold_invoice_force_test.go +++ b/itest/lnd_hold_invoice_force_test.go @@ -44,7 +44,7 @@ func testHoldInvoiceForceClose(ht *lntest.HarnessTest) { TimeoutSeconds: 60, FeeLimitMsat: noFeeLimitMsat, } - alice.RPC.SendPayment(req) + ht.SendPaymentAssertInflight(alice, req) ht.AssertInvoiceState(stream, lnrpc.Invoice_ACCEPTED) diff --git a/itest/lnd_htlc_timeout_resolver_test.go b/itest/lnd_htlc_timeout_resolver_test.go index 71a44966ed4..cd1dd3d483a 100644 --- a/itest/lnd_htlc_timeout_resolver_test.go +++ b/itest/lnd_htlc_timeout_resolver_test.go @@ -131,7 +131,7 @@ func testHtlcTimeoutResolverExtractPreimageRemote(ht *lntest.HarnessTest) { TimeoutSeconds: 60, FeeLimitMsat: noFeeLimitMsat, } - alice.RPC.SendPayment(req) + ht.SendPaymentAssertInflight(alice, req) // Once the payment sent, Alice should have one outgoing HTLC active. ht.AssertOutgoingHTLCActive(alice, aliceChanPoint, payHash[:]) @@ -270,7 +270,7 @@ func testHtlcTimeoutResolverExtractPreimageLocal(ht *lntest.HarnessTest) { TimeoutSeconds: 60, FeeLimitMsat: noFeeLimitMsat, } - alice.RPC.SendPayment(req) + ht.SendPaymentAssertInflight(alice, req) // Once the payment sent, Alice should have one outgoing HTLC active. ht.AssertOutgoingHTLCActive(alice, aliceChanPoint, payHash[:]) diff --git a/itest/lnd_misc_test.go b/itest/lnd_misc_test.go index 02e3386e6a8..33a7067552e 100644 --- a/itest/lnd_misc_test.go +++ b/itest/lnd_misc_test.go @@ -632,8 +632,10 @@ func testRejectHTLC(ht *lntest.HarnessTest) { TimeoutSeconds: 60, FeeLimitMsat: noFeeLimitMsat, } - payStream := alice.RPC.SendPayment(paymentReq) - ht.AssertPaymentStatusFromStream(payStream, lnrpc.Payment_FAILED) + ht.SendPaymentAssertFail( + alice, paymentReq, + lnrpc.PaymentFailureReason_FAILURE_REASON_NO_ROUTE, + ) ht.AssertLastHTLCError(alice, lnrpc.Failure_CHANNEL_DISABLED) diff --git a/itest/lnd_multi-hop_force_close_test.go b/itest/lnd_multi-hop_force_close_test.go index 46631bbda93..99705ff44c0 100644 --- a/itest/lnd_multi-hop_force_close_test.go +++ b/itest/lnd_multi-hop_force_close_test.go @@ -306,7 +306,7 @@ func runLocalClaimOutgoingHTLC(ht *lntest.HarnessTest, routeHints = makeRouteHints(bob, carol, params.ZeroConf) } - alice.RPC.SendPayment(&routerrpc.SendPaymentRequest{ + req := &routerrpc.SendPaymentRequest{ Dest: carolPubKey, Amt: int64(dustHtlcAmt), PaymentHash: dustPayHash, @@ -314,9 +314,10 @@ func runLocalClaimOutgoingHTLC(ht *lntest.HarnessTest, TimeoutSeconds: 60, FeeLimitMsat: noFeeLimitMsat, RouteHints: routeHints, - }) + } + ht.SendPaymentAssertInflight(alice, req) - alice.RPC.SendPayment(&routerrpc.SendPaymentRequest{ + req = &routerrpc.SendPaymentRequest{ Dest: carolPubKey, Amt: int64(htlcAmt), PaymentHash: payHash, @@ -324,7 +325,8 @@ func runLocalClaimOutgoingHTLC(ht *lntest.HarnessTest, TimeoutSeconds: 60, FeeLimitMsat: noFeeLimitMsat, RouteHints: routeHints, - }) + } + ht.SendPaymentAssertInflight(alice, req) // Verify that all nodes in the path now have two HTLC's with the // proper parameters. @@ -653,7 +655,7 @@ func runMultiHopReceiverPreimageClaim(ht *lntest.HarnessTest, TimeoutSeconds: 60, FeeLimitMsat: noFeeLimitMsat, } - alice.RPC.SendPayment(req) + ht.SendPaymentAssertInflight(alice, req) // At this point, all 3 nodes should now have an active channel with // the created HTLC pending on all of them. @@ -1014,7 +1016,7 @@ func runLocalForceCloseBeforeHtlcTimeout(ht *lntest.HarnessTest, FeeLimitMsat: noFeeLimitMsat, RouteHints: routeHints, } - alice.RPC.SendPayment(req) + ht.SendPaymentAssertInflight(alice, req) // Once the HTLC has cleared, all channels in our mini network should // have the it locked in. @@ -1343,7 +1345,7 @@ func runRemoteForceCloseBeforeHtlcTimeout(ht *lntest.HarnessTest, TimeoutSeconds: 60, FeeLimitMsat: noFeeLimitMsat, } - alice.RPC.SendPayment(req) + ht.SendPaymentAssertInflight(alice, req) // Once the HTLC has cleared, all the nodes in our mini network should // show that the HTLC has been locked in. @@ -1602,7 +1604,7 @@ func runLocalClaimIncomingHTLC(ht *lntest.HarnessTest, TimeoutSeconds: 60, FeeLimitMsat: noFeeLimitMsat, } - alice.RPC.SendPayment(req) + ht.SendPaymentAssertInflight(alice, req) // At this point, all 3 nodes should now have an active channel with // the created HTLC pending on all of them. @@ -1913,7 +1915,7 @@ func runLocalClaimIncomingHTLCLeased(ht *lntest.HarnessTest, TimeoutSeconds: 60, FeeLimitMsat: noFeeLimitMsat, } - alice.RPC.SendPayment(req) + ht.SendPaymentAssertInflight(alice, req) // At this point, all 3 nodes should now have an active channel with // the created HTLC pending on all of them. @@ -2268,7 +2270,7 @@ func runLocalPreimageClaim(ht *lntest.HarnessTest, TimeoutSeconds: 60, FeeLimitMsat: noFeeLimitMsat, } - alice.RPC.SendPayment(req) + ht.SendPaymentAssertInflight(alice, req) // At this point, all 3 nodes should now have an active channel with // the created HTLC pending on all of them. @@ -2551,7 +2553,7 @@ func runLocalPreimageClaimLeased(ht *lntest.HarnessTest, TimeoutSeconds: 60, FeeLimitMsat: noFeeLimitMsat, } - alice.RPC.SendPayment(req) + ht.SendPaymentAssertInflight(alice, req) // At this point, all 3 nodes should now have an active channel with // the created HTLC pending on all of them. @@ -2998,7 +3000,7 @@ func runHtlcAggregation(ht *lntest.HarnessTest, TimeoutSeconds: 60, FeeLimitMsat: noFeeLimitMsat, } - alice.RPC.SendPayment(req) + ht.SendPaymentAssertInflight(alice, req) } // And Carol will pay Alice's. @@ -3008,7 +3010,7 @@ func runHtlcAggregation(ht *lntest.HarnessTest, TimeoutSeconds: 60, FeeLimitMsat: noFeeLimitMsat, } - carol.RPC.SendPayment(req) + ht.SendPaymentAssertInflight(carol, req) } // At this point, all 3 nodes should now the HTLCs active on their diff --git a/itest/lnd_watchtower_test.go b/itest/lnd_watchtower_test.go index 80a0eae5a35..a68df852bcd 100644 --- a/itest/lnd_watchtower_test.go +++ b/itest/lnd_watchtower_test.go @@ -655,17 +655,12 @@ func generateBackups(ht *lntest.HarnessTest, srcNode, ) send := func(node *node.HarnessNode, payReq string) { - stream := node.RPC.SendPayment( - &routerrpc.SendPaymentRequest{ - PaymentRequest: payReq, - TimeoutSeconds: 60, - FeeLimitMsat: noFeeLimitMsat, - }, - ) - - ht.AssertPaymentStatusFromStream( - stream, lnrpc.Payment_SUCCEEDED, - ) + req := &routerrpc.SendPaymentRequest{ + PaymentRequest: payReq, + TimeoutSeconds: 60, + FeeLimitMsat: noFeeLimitMsat, + } + ht.SendPaymentAssertSettled(node, req) } // Pay each invoice. From c4a6abb14dc3553b4df8134daf6808ec2b34db3c Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 5 Nov 2024 08:17:48 +0800 Subject: [PATCH 90/92] lntest+itest: remove the usage of `ht.AssertActiveHtlcs` The method `AssertActiveHtlcs` is now removed due to it's easy to be misused. To assert a given htlc, use `AssertOutgoingHTLCActive` and `AssertIncomingHTLCActive` instead for ensuring the HTLC exists in the right direction. Although often the case `AssertNumActiveHtlcs` would be enough as it implicitly checks the forwarding behavior for an intermediate node by asserting there are always num_payment*2 HTLCs. --- itest/lnd_hold_invoice_force_test.go | 8 +- itest/lnd_multi-hop_force_close_test.go | 149 ++++++++++++++++++------ lntest/harness_assertion.go | 52 --------- 3 files changed, 117 insertions(+), 92 deletions(-) diff --git a/itest/lnd_hold_invoice_force_test.go b/itest/lnd_hold_invoice_force_test.go index 8cdd8a5298f..d9b85171077 100644 --- a/itest/lnd_hold_invoice_force_test.go +++ b/itest/lnd_hold_invoice_force_test.go @@ -50,8 +50,12 @@ func testHoldInvoiceForceClose(ht *lntest.HarnessTest) { // Once the HTLC has cleared, alice and bob should both have a single // htlc locked in. - ht.AssertActiveHtlcs(alice, payHash[:]) - ht.AssertActiveHtlcs(bob, payHash[:]) + // + // Alice should have one outgoing HTLCs on channel Alice -> Bob. + ht.AssertOutgoingHTLCActive(alice, chanPoint, payHash[:]) + + // Bob should have one incoming HTLC on channel Alice -> Bob. + ht.AssertIncomingHTLCActive(bob, chanPoint, payHash[:]) // Get our htlc expiry height and current block height so that we // can mine the exact number of blocks required to expire the htlc. diff --git a/itest/lnd_multi-hop_force_close_test.go b/itest/lnd_multi-hop_force_close_test.go index 99705ff44c0..fc417c5d748 100644 --- a/itest/lnd_multi-hop_force_close_test.go +++ b/itest/lnd_multi-hop_force_close_test.go @@ -328,11 +328,18 @@ func runLocalClaimOutgoingHTLC(ht *lntest.HarnessTest, } ht.SendPaymentAssertInflight(alice, req) - // Verify that all nodes in the path now have two HTLC's with the - // proper parameters. - ht.AssertActiveHtlcs(alice, dustPayHash, payHash) - ht.AssertActiveHtlcs(bob, dustPayHash, payHash) - ht.AssertActiveHtlcs(carol, dustPayHash, payHash) + // At this point, all 3 nodes should now have an active channel with + // the created HTLC pending on all of them. + // + // Alice should have two outgoing HTLCs on channel Alice -> Bob. + ht.AssertNumActiveHtlcs(alice, 2) + + // Bob should have two incoming HTLCs on channel Alice -> Bob, and two + // outgoing HTLCs on channel Bob -> Carol. + ht.AssertNumActiveHtlcs(bob, 4) + + // Carol should have two incoming HTLCs on channel Bob -> Carol. + ht.AssertNumActiveHtlcs(carol, 2) // We'll now mine enough blocks to trigger Bob's force close the // channel Bob=>Carol due to the fact that the HTLC is about to @@ -372,7 +379,7 @@ func runLocalClaimOutgoingHTLC(ht *lntest.HarnessTest, // At this point, Bob should have canceled backwards the dust HTLC that // we sent earlier. This means Alice should now only have a single HTLC // on her channel. - ht.AssertActiveHtlcs(alice, payHash) + ht.AssertNumActiveHtlcs(alice, 1) // With the closing transaction confirmed, we should expect Bob's HTLC // timeout transaction to be offered to the sweeper due to the expiry @@ -659,9 +666,18 @@ func runMultiHopReceiverPreimageClaim(ht *lntest.HarnessTest, // At this point, all 3 nodes should now have an active channel with // the created HTLC pending on all of them. - ht.AssertActiveHtlcs(alice, payHash[:]) - ht.AssertActiveHtlcs(bob, payHash[:]) - ht.AssertActiveHtlcs(carol, payHash[:]) + // At this point, all 3 nodes should now have an active channel with + // the created HTLCs pending on all of them. + // + // Alice should have one outgoing HTLCs on channel Alice -> Bob. + ht.AssertNumActiveHtlcs(alice, 1) + + // Bob should have one incoming HTLC on channel Alice -> Bob, and one + // outgoing HTLC on channel Bob -> Carol. + ht.AssertNumActiveHtlcs(bob, 2) + + // Carol should have one incoming HTLC on channel Bob -> Carol. + ht.AssertNumActiveHtlcs(carol, 1) // Wait for Carol to mark invoice as accepted. There is a small gap to // bridge between adding the htlc to the channel and executing the exit @@ -1018,11 +1034,20 @@ func runLocalForceCloseBeforeHtlcTimeout(ht *lntest.HarnessTest, } ht.SendPaymentAssertInflight(alice, req) - // Once the HTLC has cleared, all channels in our mini network should - // have the it locked in. - ht.AssertActiveHtlcs(alice, payHash) - ht.AssertActiveHtlcs(bob, payHash) - ht.AssertActiveHtlcs(carol, payHash) + // At this point, all 3 nodes should now have an active channel with + // the created HTLC pending on all of them. + // At this point, all 3 nodes should now have an active channel with + // the created HTLCs pending on all of them. + // + // Alice should have one outgoing HTLC on channel Alice -> Bob. + ht.AssertNumActiveHtlcs(alice, 1) + + // Bob should have one incoming HTLC on channel Alice -> Bob, and one + // outgoing HTLC on channel Bob -> Carol. + ht.AssertNumActiveHtlcs(bob, 2) + + // Carol should have one incoming HTLC on channel Bob -> Carol. + ht.AssertNumActiveHtlcs(carol, 1) // Now that all parties have the HTLC locked in, we'll immediately // force close the Bob -> Carol channel. This should trigger contract @@ -1347,11 +1372,18 @@ func runRemoteForceCloseBeforeHtlcTimeout(ht *lntest.HarnessTest, } ht.SendPaymentAssertInflight(alice, req) - // Once the HTLC has cleared, all the nodes in our mini network should - // show that the HTLC has been locked in. - ht.AssertActiveHtlcs(alice, payHash[:]) - ht.AssertActiveHtlcs(bob, payHash[:]) - ht.AssertActiveHtlcs(carol, payHash[:]) + // At this point, all 3 nodes should now have an active channel with + // the created HTLCs pending on all of them. + // + // Alice should have one outgoing HTLC on channel Alice -> Bob. + ht.AssertNumActiveHtlcs(alice, 1) + + // Bob should have one incoming HTLC on channel Alice -> Bob, and one + // outgoing HTLC on channel Bob -> Carol. + ht.AssertNumActiveHtlcs(bob, 2) + + // Carol should have one incoming HTLC on channel Bob -> Carol. + ht.AssertNumActiveHtlcs(carol, 1) // At this point, we'll now instruct Carol to force close the tx. This // will let us exercise that Bob is able to sweep the expired HTLC on @@ -1608,9 +1640,18 @@ func runLocalClaimIncomingHTLC(ht *lntest.HarnessTest, // At this point, all 3 nodes should now have an active channel with // the created HTLC pending on all of them. - ht.AssertActiveHtlcs(alice, payHash[:]) - ht.AssertActiveHtlcs(bob, payHash[:]) - ht.AssertActiveHtlcs(carol, payHash[:]) + // At this point, all 3 nodes should now have an active channel with + // the created HTLCs pending on all of them. + // + // Alice should have one outgoing HTLC on channel Alice -> Bob. + ht.AssertNumActiveHtlcs(alice, 1) + + // Bob should have one incoming HTLC on channel Alice -> Bob, and one + // outgoing HTLC on channel Bob -> Carol. + ht.AssertNumActiveHtlcs(bob, 2) + + // Carol should have one incoming HTLC on channel Bob -> Carol. + ht.AssertNumActiveHtlcs(carol, 1) // Wait for carol to mark invoice as accepted. There is a small gap to // bridge between adding the htlc to the channel and executing the exit @@ -1919,9 +1960,18 @@ func runLocalClaimIncomingHTLCLeased(ht *lntest.HarnessTest, // At this point, all 3 nodes should now have an active channel with // the created HTLC pending on all of them. - ht.AssertActiveHtlcs(alice, payHash[:]) - ht.AssertActiveHtlcs(bob, payHash[:]) - ht.AssertActiveHtlcs(carol, payHash[:]) + // At this point, all 3 nodes should now have an active channel with + // the created HTLCs pending on all of them. + // + // Alice should have one outgoing HTLC on channel Alice -> Bob. + ht.AssertNumActiveHtlcs(alice, 1) + + // Bob should have one incoming HTLC on channel Alice -> Bob, and one + // outgoing HTLC on channel Bob -> Carol. + ht.AssertNumActiveHtlcs(bob, 2) + + // Carol should have one incoming HTLC on channel Bob -> Carol. + ht.AssertNumActiveHtlcs(carol, 1) // Wait for carol to mark invoice as accepted. There is a small gap to // bridge between adding the htlc to the channel and executing the exit @@ -2273,10 +2323,17 @@ func runLocalPreimageClaim(ht *lntest.HarnessTest, ht.SendPaymentAssertInflight(alice, req) // At this point, all 3 nodes should now have an active channel with - // the created HTLC pending on all of them. - ht.AssertActiveHtlcs(alice, payHash[:]) - ht.AssertActiveHtlcs(bob, payHash[:]) - ht.AssertActiveHtlcs(carol, payHash[:]) + // the created HTLCs pending on all of them. + // + // Alice should have one outgoing HTLC on channel Alice -> Bob. + ht.AssertNumActiveHtlcs(alice, 1) + + // Bob should have one incoming HTLC on channel Alice -> Bob, and one + // outgoing HTLC on channel Bob -> Carol. + ht.AssertNumActiveHtlcs(bob, 2) + + // Carol should have one incoming HTLC on channel Bob -> Carol. + ht.AssertNumActiveHtlcs(carol, 1) // Wait for carol to mark invoice as accepted. There is a small gap to // bridge between adding the htlc to the channel and executing the exit @@ -2556,10 +2613,17 @@ func runLocalPreimageClaimLeased(ht *lntest.HarnessTest, ht.SendPaymentAssertInflight(alice, req) // At this point, all 3 nodes should now have an active channel with - // the created HTLC pending on all of them. - ht.AssertActiveHtlcs(alice, payHash[:]) - ht.AssertActiveHtlcs(bob, payHash[:]) - ht.AssertActiveHtlcs(carol, payHash[:]) + // the created HTLCs pending on all of them. + // + // Alice should have one outgoing HTLC on channel Alice -> Bob. + ht.AssertNumActiveHtlcs(alice, 1) + + // Bob should have one incoming HTLC on channel Alice -> Bob, and one + // outgoing HTLC on channel Bob -> Carol. + ht.AssertNumActiveHtlcs(bob, 2) + + // Carol should have one incoming HTLC on channel Bob -> Carol. + ht.AssertNumActiveHtlcs(carol, 1) // Wait for carol to mark invoice as accepted. There is a small gap to // bridge between adding the htlc to the channel and executing the exit @@ -3013,11 +3077,20 @@ func runHtlcAggregation(ht *lntest.HarnessTest, ht.SendPaymentAssertInflight(carol, req) } - // At this point, all 3 nodes should now the HTLCs active on their - // channels. - ht.AssertActiveHtlcs(alice, payHashes...) - ht.AssertActiveHtlcs(bob, payHashes...) - ht.AssertActiveHtlcs(carol, payHashes...) + // At this point, all 3 nodes should now have an active channel with + // the created HTLCs pending on all of them. + // + // Alice sent numInvoices and received numInvoices payments, she should + // have numInvoices*2 HTLCs. + ht.AssertNumActiveHtlcs(alice, numInvoices*2) + + // Bob should have 2*numInvoices HTLCs on channel Alice -> Bob, and + // numInvoices*2 HTLCs on channel Bob -> Carol. + ht.AssertNumActiveHtlcs(bob, numInvoices*4) + + // Carol sent numInvoices and received numInvoices payments, she should + // have numInvoices*2 HTLCs. + ht.AssertNumActiveHtlcs(carol, numInvoices*2) // Wait for Alice and Carol to mark the invoices as accepted. There is // a small gap to bridge between adding the htlc to the channel and diff --git a/lntest/harness_assertion.go b/lntest/harness_assertion.go index 1d081bc1260..b99f461d185 100644 --- a/lntest/harness_assertion.go +++ b/lntest/harness_assertion.go @@ -1314,58 +1314,6 @@ func (h *HarnessTest) AssertNumActiveHtlcs(hn *node.HarnessNode, num int) { hn.Name()) } -// AssertActiveHtlcs makes sure the node has the _exact_ HTLCs matching -// payHashes on _all_ their channels. -func (h *HarnessTest) AssertActiveHtlcs(hn *node.HarnessNode, - payHashes ...[]byte) { - - err := wait.NoError(func() error { - // We require the RPC call to be succeeded and won't wait for - // it as it's an unexpected behavior. - req := &lnrpc.ListChannelsRequest{} - nodeChans := hn.RPC.ListChannels(req) - - for _, ch := range nodeChans.Channels { - // Record all payment hashes active for this channel. - htlcHashes := make(map[string]struct{}) - - for _, htlc := range ch.PendingHtlcs { - h := hex.EncodeToString(htlc.HashLock) - _, ok := htlcHashes[h] - if ok { - return fmt.Errorf("duplicate HashLock "+ - "in PendingHtlcs: %v", - ch.PendingHtlcs) - } - htlcHashes[h] = struct{}{} - } - - // Channel should have exactly the payHashes active. - if len(payHashes) != len(htlcHashes) { - return fmt.Errorf("node [%s:%x] had %v "+ - "htlcs active, expected %v", - hn.Name(), hn.PubKey[:], - len(htlcHashes), len(payHashes)) - } - - // Make sure all the payHashes are active. - for _, payHash := range payHashes { - h := hex.EncodeToString(payHash) - if _, ok := htlcHashes[h]; ok { - continue - } - - return fmt.Errorf("node [%s:%x] didn't have: "+ - "the payHash %v active", hn.Name(), - hn.PubKey[:], h) - } - } - - return nil - }, DefaultTimeout) - require.NoError(h, err, "timeout checking active HTLCs") -} - // AssertIncomingHTLCActive asserts the node has a pending incoming HTLC in the // given channel. Returns the HTLC if found and active. func (h *HarnessTest) AssertIncomingHTLCActive(hn *node.HarnessNode, From a1bd8943db78d1148f205129a95a9dacb54be77b Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 20 Dec 2024 10:58:26 +0800 Subject: [PATCH 91/92] lntest+itest: export `DeriveFundingShim` --- itest/lnd_channel_backup_test.go | 4 +- itest/lnd_funding_test.go | 137 ++----------------------------- lntest/harness.go | 6 +- 3 files changed, 13 insertions(+), 134 deletions(-) diff --git a/itest/lnd_channel_backup_test.go b/itest/lnd_channel_backup_test.go index d6e32d368f4..4ea5f6933ba 100644 --- a/itest/lnd_channel_backup_test.go +++ b/itest/lnd_channel_backup_test.go @@ -637,8 +637,8 @@ func runChanRestoreScenarioCommitTypes(ht *lntest.HarnessTest, minerHeight := ht.CurrentHeight() thawHeight := minerHeight + thawHeightDelta - fundingShim, _ = deriveFundingShim( - ht, dave, carol, crs.params.Amt, thawHeight, true, ct, + fundingShim, _ = ht.DeriveFundingShim( + dave, carol, crs.params.Amt, thawHeight, true, ct, ) crs.params.FundingShim = fundingShim } diff --git a/itest/lnd_funding_test.go b/itest/lnd_funding_test.go index 074e53ef7e4..4c045776e06 100644 --- a/itest/lnd_funding_test.go +++ b/itest/lnd_funding_test.go @@ -6,18 +6,13 @@ import ( "testing" "time" - "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" - "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/chainreg" - "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/funding" - "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/labels" "github.com/lightningnetwork/lnd/lnrpc" - "github.com/lightningnetwork/lnd/lnrpc/signrpc" "github.com/lightningnetwork/lnd/lntest" "github.com/lightningnetwork/lnd/lntest/node" "github.com/lightningnetwork/lnd/lnwallet/chainfee" @@ -583,8 +578,8 @@ func runExternalFundingScriptEnforced(ht *lntest.HarnessTest) { // a transaction that will never be published. const thawHeight uint32 = 10 const chanSize = funding.MaxBtcFundingAmount - fundingShim1, chanPoint1 := deriveFundingShim( - ht, carol, dave, chanSize, thawHeight, false, commitmentType, + fundingShim1, chanPoint1 := ht.DeriveFundingShim( + carol, dave, chanSize, thawHeight, false, commitmentType, ) ht.OpenChannelAssertPending( carol, dave, lntest.OpenChannelParams{ @@ -599,8 +594,8 @@ func runExternalFundingScriptEnforced(ht *lntest.HarnessTest) { // externally funded, we should still be able to open another one. Let's // do exactly that now. For this one we publish the transaction so we // can mine it later. - fundingShim2, chanPoint2 := deriveFundingShim( - ht, carol, dave, chanSize, thawHeight, true, commitmentType, + fundingShim2, chanPoint2 := ht.DeriveFundingShim( + carol, dave, chanSize, thawHeight, true, commitmentType, ) // At this point, we'll now carry out the normal basic channel funding @@ -699,8 +694,8 @@ func runExternalFundingTaproot(ht *lntest.HarnessTest) { // a transaction that will never be published. const thawHeight uint32 = 10 const chanSize = funding.MaxBtcFundingAmount - fundingShim1, chanPoint1 := deriveFundingShim( - ht, carol, dave, chanSize, thawHeight, false, commitmentType, + fundingShim1, chanPoint1 := ht.DeriveFundingShim( + carol, dave, chanSize, thawHeight, false, commitmentType, ) ht.OpenChannelAssertPending(carol, dave, lntest.OpenChannelParams{ Amt: chanSize, @@ -715,8 +710,8 @@ func runExternalFundingTaproot(ht *lntest.HarnessTest) { // externally funded, we should still be able to open another one. Let's // do exactly that now. For this one we publish the transaction so we // can mine it later. - fundingShim2, chanPoint2 := deriveFundingShim( - ht, carol, dave, chanSize, thawHeight, true, commitmentType, + fundingShim2, chanPoint2 := ht.DeriveFundingShim( + carol, dave, chanSize, thawHeight, true, commitmentType, ) // At this point, we'll now carry out the normal basic channel funding @@ -1163,122 +1158,6 @@ func ensurePolicy(ht *lntest.HarnessTest, alice, peer *node.HarnessNode, require.EqualValues(ht, expectedFeeRate, alicePolicy.FeeRateMilliMsat) } -// deriveFundingShim creates a channel funding shim by deriving the necessary -// keys on both sides. -func deriveFundingShim(ht *lntest.HarnessTest, carol, dave *node.HarnessNode, - chanSize btcutil.Amount, thawHeight uint32, publish bool, - commitType lnrpc.CommitmentType) (*lnrpc.FundingShim, - *lnrpc.ChannelPoint) { - - keyLoc := &signrpc.KeyLocator{KeyFamily: 9999} - carolFundingKey := carol.RPC.DeriveKey(keyLoc) - daveFundingKey := dave.RPC.DeriveKey(keyLoc) - - // Now that we have the multi-sig keys for each party, we can manually - // construct the funding transaction. We'll instruct the backend to - // immediately create and broadcast a transaction paying out an exact - // amount. Normally this would reside in the mempool, but we just - // confirm it now for simplicity. - var ( - fundingOutput *wire.TxOut - musig2 bool - err error - ) - if commitType == lnrpc.CommitmentType_SIMPLE_TAPROOT { - var carolKey, daveKey *btcec.PublicKey - carolKey, err = btcec.ParsePubKey(carolFundingKey.RawKeyBytes) - require.NoError(ht, err) - daveKey, err = btcec.ParsePubKey(daveFundingKey.RawKeyBytes) - require.NoError(ht, err) - - _, fundingOutput, err = input.GenTaprootFundingScript( - carolKey, daveKey, int64(chanSize), - fn.None[chainhash.Hash](), - ) - require.NoError(ht, err) - - musig2 = true - } else { - _, fundingOutput, err = input.GenFundingPkScript( - carolFundingKey.RawKeyBytes, daveFundingKey.RawKeyBytes, - int64(chanSize), - ) - require.NoError(ht, err) - } - - var txid *chainhash.Hash - targetOutputs := []*wire.TxOut{fundingOutput} - if publish { - txid = ht.SendOutputsWithoutChange(targetOutputs, 5) - } else { - tx := ht.CreateTransaction(targetOutputs, 5) - - txHash := tx.TxHash() - txid = &txHash - } - - // At this point, we can being our external channel funding workflow. - // We'll start by generating a pending channel ID externally that will - // be used to track this new funding type. - pendingChanID := ht.Random32Bytes() - - // Now that we have the pending channel ID, Dave (our responder) will - // register the intent to receive a new channel funding workflow using - // the pending channel ID. - chanPoint := &lnrpc.ChannelPoint{ - FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{ - FundingTxidBytes: txid[:], - }, - } - chanPointShim := &lnrpc.ChanPointShim{ - Amt: int64(chanSize), - ChanPoint: chanPoint, - LocalKey: &lnrpc.KeyDescriptor{ - RawKeyBytes: daveFundingKey.RawKeyBytes, - KeyLoc: &lnrpc.KeyLocator{ - KeyFamily: daveFundingKey.KeyLoc.KeyFamily, - KeyIndex: daveFundingKey.KeyLoc.KeyIndex, - }, - }, - RemoteKey: carolFundingKey.RawKeyBytes, - PendingChanId: pendingChanID, - ThawHeight: thawHeight, - Musig2: musig2, - } - fundingShim := &lnrpc.FundingShim{ - Shim: &lnrpc.FundingShim_ChanPointShim{ - ChanPointShim: chanPointShim, - }, - } - dave.RPC.FundingStateStep(&lnrpc.FundingTransitionMsg{ - Trigger: &lnrpc.FundingTransitionMsg_ShimRegister{ - ShimRegister: fundingShim, - }, - }) - - // If we attempt to register the same shim (has the same pending chan - // ID), then we should get an error. - dave.RPC.FundingStateStepAssertErr(&lnrpc.FundingTransitionMsg{ - Trigger: &lnrpc.FundingTransitionMsg_ShimRegister{ - ShimRegister: fundingShim, - }, - }) - - // We'll take the chan point shim we just registered for Dave (the - // responder), and swap the local/remote keys before we feed it in as - // Carol's funding shim as the initiator. - fundingShim.GetChanPointShim().LocalKey = &lnrpc.KeyDescriptor{ - RawKeyBytes: carolFundingKey.RawKeyBytes, - KeyLoc: &lnrpc.KeyLocator{ - KeyFamily: carolFundingKey.KeyLoc.KeyFamily, - KeyIndex: carolFundingKey.KeyLoc.KeyIndex, - }, - } - fundingShim.GetChanPointShim().RemoteKey = daveFundingKey.RawKeyBytes - - return fundingShim, chanPoint -} - // testChannelFundingWithUnstableUtxos tests channel openings with restricted // utxo selection. Internal wallet utxos might be restricted due to another // subsystems still using it therefore it would be unsecure to use them for diff --git a/lntest/harness.go b/lntest/harness.go index 91331370e50..606b64351be 100644 --- a/lntest/harness.go +++ b/lntest/harness.go @@ -2348,7 +2348,7 @@ func (h *HarnessTest) openChannelsForNodes(nodes []*node.HarnessNode, // open channel params and return it. minerHeight := h.CurrentHeight() thawHeight := minerHeight + thawHeightDelta - fundingShim, _ := h.deriveFundingShim( + fundingShim, _ := h.DeriveFundingShim( nodeA, nodeB, p.Amt, thawHeight, true, leasedType, ) @@ -2417,9 +2417,9 @@ func (h *HarnessTest) openZeroConfChannelsForNodes(nodes []*node.HarnessNode, return resp } -// deriveFundingShim creates a channel funding shim by deriving the necessary +// DeriveFundingShim creates a channel funding shim by deriving the necessary // keys on both sides. -func (h *HarnessTest) deriveFundingShim(alice, bob *node.HarnessNode, +func (h *HarnessTest) DeriveFundingShim(alice, bob *node.HarnessNode, chanSize btcutil.Amount, thawHeight uint32, publish bool, commitType lnrpc.CommitmentType) (*lnrpc.FundingShim, *lnrpc.ChannelPoint) { From ecd82a3bcbc04c34fe425e81a3df547a0f3207f1 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Wed, 18 Dec 2024 03:30:39 +0800 Subject: [PATCH 92/92] contractcourt: include custom records on replayed htlc Add another case in addition to #9357. --- contractcourt/htlc_incoming_contest_resolver.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contractcourt/htlc_incoming_contest_resolver.go b/contractcourt/htlc_incoming_contest_resolver.go index c1383a947ca..95d08c417f8 100644 --- a/contractcourt/htlc_incoming_contest_resolver.go +++ b/contractcourt/htlc_incoming_contest_resolver.go @@ -664,7 +664,7 @@ func (h *htlcIncomingContestResolver) findAndapplyPreimage() (bool, error) { // via the preimage beacon. resolution, err := h.Registry.NotifyExitHopHtlc( h.htlc.RHash, h.htlc.Amt, h.htlcExpiry, 0, - circuitKey, nil, nil, payload, + circuitKey, nil, h.htlc.CustomRecords, payload, ) if err != nil { return false, err